From b534c7c898298c64d70afb86f2e03069d2f5c268 Mon Sep 17 00:00:00 2001 From: Tushar Sharma Date: Thu, 13 Mar 2025 10:26:34 -0700 Subject: [PATCH 001/230] [test]: Fix L0_batch_custom - missing CMake variables that could result in defaulting to the wrong main branch (#8069) --- qa/L0_batch_custom/test.sh | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/qa/L0_batch_custom/test.sh b/qa/L0_batch_custom/test.sh index 13d54c25d8..6a2e23c544 100755 --- a/qa/L0_batch_custom/test.sh +++ b/qa/L0_batch_custom/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -87,8 +87,10 @@ git clone --single-branch --depth=1 -b $TRITON_BACKEND_REPO_TAG \ mkdir build && cd build && cmake -DCMAKE_INSTALL_PREFIX:PATH=`pwd`/install \ - -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ - -DTRITON_CORE_REPO_TAG=$TRITON_CORE_REPO_TAG .. && + -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ + -DTRITON_BACKEND_REPO_TAG=${TRITON_BACKEND_REPO_TAG} \ + -DTRITON_CORE_REPO_TAG=${TRITON_CORE_REPO_TAG} \ + -DTRITON_COMMON_REPO_TAG=${TRITON_COMMON_REPO_TAG} .. && make -j4 install) (cd backend/examples/batching_strategies/single_batching && @@ -96,7 +98,9 @@ git clone --single-branch --depth=1 -b $TRITON_BACKEND_REPO_TAG \ cd build && cmake -DCMAKE_INSTALL_PREFIX:PATH=`pwd`/install \ -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ - -DTRITON_CORE_REPO_TAG=$TRITON_CORE_REPO_TAG .. && + -DTRITON_BACKEND_REPO_TAG=${TRITON_BACKEND_REPO_TAG} \ + -DTRITON_CORE_REPO_TAG=${TRITON_CORE_REPO_TAG} \ + -DTRITON_COMMON_REPO_TAG=${TRITON_COMMON_REPO_TAG} .. && make -j4 install) cp -r backend/examples/batching_strategies/volume_batching/build/libtriton_volumebatching.so models @@ -164,8 +168,10 @@ sed -i "s/${OLD_STRING}/${NEW_STRING}/g" ${FILE_PATH} (cd backend/examples/batching_strategies/volume_batching && cd build && cmake -DCMAKE_INSTALL_PREFIX:PATH=`pwd`/install \ - -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ - -DTRITON_CORE_REPO_TAG=$TRITON_CORE_REPO_TAG .. && + -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ + -DTRITON_BACKEND_REPO_TAG=${TRITON_BACKEND_REPO_TAG} \ + -DTRITON_CORE_REPO_TAG=${TRITON_CORE_REPO_TAG} \ + -DTRITON_COMMON_REPO_TAG=${TRITON_COMMON_REPO_TAG} .. && make -j4 install) cp -r backend/examples/batching_strategies/volume_batching/build/libtriton_volumebatching.so models/${MODEL_NAME}/libtriton_volumebatching.so From 45d1fb400d48758380570562ed33145b7a41183c Mon Sep 17 00:00:00 2001 From: Tushar Sharma Date: Fri, 14 Mar 2025 14:52:04 -0700 Subject: [PATCH 002/230] [test]: L0_batch_custom & L0_client_build_variants: Set default values for triton repos to pass in to cmake (#8072) --- qa/L0_batch_custom/test.sh | 2 ++ qa/L0_client_build_variants/test.sh | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/qa/L0_batch_custom/test.sh b/qa/L0_batch_custom/test.sh index 6a2e23c544..5c6824897b 100755 --- a/qa/L0_batch_custom/test.sh +++ b/qa/L0_batch_custom/test.sh @@ -54,6 +54,8 @@ TEST_RESULT_FILE='test_results.txt' TRITON_REPO_ORGANIZATION=${TRITON_REPO_ORGANIZATION:="http://github.com/triton-inference-server"} TRITON_BACKEND_REPO_TAG=${TRITON_BACKEND_REPO_TAG:="main"} TRITON_CORE_REPO_TAG=${TRITON_CORE_REPO_TAG:="main"} +TRITON_COMMON_REPO_TAG=${TRITON_COMMON_REPO_TAG:="main"} + source ../common/util.sh RET=0 diff --git a/qa/L0_client_build_variants/test.sh b/qa/L0_client_build_variants/test.sh index a64d698526..6ff7eb4916 100755 --- a/qa/L0_client_build_variants/test.sh +++ b/qa/L0_client_build_variants/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -50,6 +50,10 @@ mkdir -p /workspace/build # Build without GPU support # TRITON_REPO_ORGANIZATION=${TRITON_REPO_ORGANIZATION:="http://github.com/triton-inference-server"} +TRITON_BACKEND_REPO_TAG=${TRITON_BACKEND_REPO_TAG:="main"} +TRITON_CORE_REPO_TAG=${TRITON_CORE_REPO_TAG:="main"} +TRITON_COMMON_REPO_TAG=${TRITON_COMMON_REPO_TAG:="main"} + (cd /workspace/build && \ rm -fr cc-clients java-clients python-clients && \ cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ From 928fd7ec1907248d3355073b3392daa95d57ea22 Mon Sep 17 00:00:00 2001 From: BenjaminBraunDev Date: Sun, 16 Mar 2025 23:00:07 -0700 Subject: [PATCH 003/230] feat: ORCA Format KV Cache Utilization in Inference Response Header (#7839) --- qa/common/orca_header_test.py | 167 ++++++++++++++++++++++++ src/CMakeLists.txt | 4 +- src/http_server.cc | 32 ++++- src/http_server.h | 7 +- src/orca_http.cc | 233 ++++++++++++++++++++++++++++++++++ src/orca_http.h | 67 ++++++++++ 6 files changed, 507 insertions(+), 3 deletions(-) create mode 100755 qa/common/orca_header_test.py create mode 100644 src/orca_http.cc create mode 100644 src/orca_http.h diff --git a/qa/common/orca_header_test.py b/qa/common/orca_header_test.py new file mode 100755 index 0000000000..660b025556 --- /dev/null +++ b/qa/common/orca_header_test.py @@ -0,0 +1,167 @@ +#!/usr/bin/python3 +# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import sys + +sys.path.append("../common") + +import argparse +import json + +import requests + + +# To run the test, have tritonserver running and run this script with the endpoint as a flag. +# +# Example: +# ``` +# python3 orca_header_test.py http://localhost:8000/v2/models/ensemble/generate +# ``` +def get_endpoint_header(url, data, request_header=None): + """ + Sends a POST request to the given URL with the provided data and returns the value of the "endpoint-load-metrics" header, + or None if the request fails. + """ + HEADER_KEY = "endpoint-load-metrics" + try: + response = None + if request_header: + response = requests.post(url, json=data, headers=request_header) + else: + response = requests.post(url, json=data) + response.raise_for_status() + return response.headers.get(HEADER_KEY, "") + except requests.exceptions.RequestException as e: + print(f"Error making request: {e}") + return None + + +def parse_header_data(header, orca_format): + """ + Parses the header data into a dictionary based on the given format. + """ + METRIC_KEY = "named_metrics" + try: + if orca_format == "json": + # Parse the header in JSON format + data = json.loads(header.replace("JSON ", "")) + if METRIC_KEY in data: + return data[METRIC_KEY] + else: + print(f"No key '{METRIC_KEY}' in header data: {data}") + return None + elif orca_format == "text": + # Parse the header in TEXT format + data = {} + for key_value_pair in header.replace("TEXT ", "").split(", "): + key, value = key_value_pair.split("=") + if "." in key: + prefix, nested_key = key.split(".", 1) + if prefix == METRIC_KEY: + data[nested_key] = float(value) + if not data: + print(f"Could not parse any keys from header: {header}") + return None + return data + else: + print(f"Invalid ORCA format: {orca_format}") + return None + except (json.JSONDecodeError, ValueError, KeyError): + print("Error: Invalid data in the header.") + return None + + +def check_for_keys(data, desired_keys, orca_format): + """ + Checks if all desired keys are present in the given data dictionary. + """ + if all(key in data for key in desired_keys): + print( + "ORCA header present in ", + orca_format, + "format with" "kv_cache_utilization:", + [k + ": " + str(data[k]) for k in desired_keys], + ) + return True + else: + print(f"Missing keys in header: {', '.join(set(desired_keys) - set(data))}") + return False + + +def request_header(orca_format): + return {"endpoint-load-metrics-format": orca_format} if orca_format else None + + +def test_header_type(url, data, orca_format): + req_header = request_header(orca_format) + response_header = get_endpoint_header(args.url, TEST_DATA, req_header) + + desired_keys = { + "kv_cache_utilization", + "max_token_capacity", + } # Just the keys, no need to initialize with None + + if response_header is None: + print(f"Request to endpoint: '{args.url}' failed.") + return False + elif response_header == "": + if orca_format: + print( + f"response header empty, endpoint-load-metrics-format={orca_format} is not a valid ORCA metric format" + ) + return False + else: + # No request header set <=> no response header. Intended behavior. + print(f"response header empty, endpoint-load-metrics-format is not set") + return True + + data = parse_header_data(response_header, orca_format) + if data: + return check_for_keys(data, desired_keys, orca_format) + else: + print(f"Unexpected response header value: {response_header}") + return False + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Make a POST request to generate endpoint to test the ORCA metrics header." + ) + parser.add_argument("url", help="The model URL to send the request to.") + args = parser.parse_args() + TEST_DATA = json.loads( + '{"text_input": "hello world", "max_tokens": 20, "bad_words": "", "stop_words": ""}' + ) + passed = True + + for format in ["json", "text", None]: + print("Checking response header for ORCA format:", format) + if not test_header_type(args.url, TEST_DATA, format): + print("FAIL on format:", format) + passed = False + + sys.exit(0 if passed else 1) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 600d1bac92..8824e2ed6a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -337,10 +337,12 @@ if(${TRITON_ENABLE_HTTP} list(APPEND HTTP_ENDPOINT_SRCS http_server.cc + orca_http.cc ) list(APPEND HTTP_ENDPOINT_HDRS http_server.h + orca_http.h ) # Add header / src files based on HTTP related endpoint requested diff --git a/src/http_server.cc b/src/http_server.cc index 4f8c0453f6..edec3aae0a 100644 --- a/src/http_server.cc +++ b/src/http_server.cc @@ -1,4 +1,4 @@ -// Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -3392,6 +3392,7 @@ HTTPAPIServer::HandleGenerate( request_release_payload.release(); } + TRITONSERVER_Error* HTTPAPIServer::ModelInputMetadata( const std::string& model_name, const int64_t model_version, @@ -4238,6 +4239,35 @@ HTTPAPIServer::GenerateRequestClass::StartResponse( return; } + +#ifdef TRITON_ENABLE_METRICS + // logic to add kv_cache metrics to response header + // Get the metrics in Prometheus format + + // ENDPOINT_LOAD_METRICS_TYPE is request header that specifies which load + // report format `endpoint-load-metrics` will be in. If not present, the + // response header will not be written and the feature is disabled. + // + // The valid values for ENDPOINT_LOAD_METRICS_TYPE header are: + // + // "text" + // "json" + // + // Any other value will have behavior equivalent to being unset while also + // logging an error. + auto server = infer_request->EvHtpServer(); + const char* orca_metric_format = nullptr; + evhtp_header_t* metric_format_header = + evhtp_headers_find_header(req->headers_in, ENDPOINT_LOAD_METRICS_TYPE); + + if (metric_format_header != nullptr) { + orca_metric_format = metric_format_header->val; + } + if (orca_metric_format != nullptr && server != nullptr) { + SetEndpointLoadMetricsHeader(req, orca_metric_format, server); + } +#endif // TRITON_ENABLE_METRICS + if (infer_request->streaming_) { AddContentTypeHeader(req, "text/event-stream; charset=utf-8"); } else { diff --git a/src/http_server.h b/src/http_server.h index efadc21675..785c408041 100644 --- a/src/http_server.h +++ b/src/http_server.h @@ -1,4 +1,4 @@ -// Copyright 2020-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -39,6 +39,7 @@ #include "common.h" #include "data_compressor.h" +#include "orca_http.h" #include "restricted_features.h" #include "shared_memory_manager.h" #include "tracer.h" @@ -397,6 +398,8 @@ class HTTPAPIServer : public HTTPServer { } virtual ~GenerateRequestClass(); + TRITONSERVER_Server* EvHtpServer() const { return server_; } + // [FIXME] Specialize response complete function for now, should have // been a dispatcher and call into object specific response function. static void InferResponseComplete( @@ -436,6 +439,7 @@ class HTTPAPIServer : public HTTPServer { // TENSOR, PARAMETER type uint32_t index; }; + TRITONSERVER_Error* ExactMappingInput( const std::string& name, triton::common::TritonJson::Value& value, std::map& @@ -488,6 +492,7 @@ class HTTPAPIServer : public HTTPServer { evbuffer* buffer_ = nullptr; }; + protected: explicit HTTPAPIServer( const std::shared_ptr& server, diff --git a/src/orca_http.cc b/src/orca_http.cc new file mode 100644 index 0000000000..082a17180e --- /dev/null +++ b/src/orca_http.cc @@ -0,0 +1,233 @@ +// Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of NVIDIA CORPORATION nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +// OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "orca_http.h" + +void +SetEndpointLoadMetricsHeader( + evhtp_request_t* req, const char* orca_metric_format, + TRITONSERVER_Server* server) +{ + const std::string orca_type = orca_metric_format; + TRITONSERVER_Metrics* metrics = nullptr; + TRITONSERVER_Error* err = TRITONSERVER_ServerMetrics(server, &metrics); + if (err == nullptr) { + const char* base; + size_t byte_size; + err = TRITONSERVER_MetricsFormatted( + metrics, TRITONSERVER_METRIC_PROMETHEUS, &base, &byte_size); + if (err == nullptr) { + std::string formatted_metrics(base, byte_size); + // Extract the KV utilization metrics from the Prometheus formatted + // string. + std::string extracted_kv_metrics = + ExtractKVMetrics(formatted_metrics, orca_type); + if (!extracted_kv_metrics.empty()) { + evhtp_headers_add_header( + req->headers_out, evhtp_header_new( + ENDPOINT_LOAD_METRICS_NAME, + extracted_kv_metrics.c_str(), 1, 1)); + } else { + LOG_ERROR << "ENDPOINT_LOAD_METRICS_TYPE request header is set but " + "extracted_kv_metrics is " + "empty, no header written. orca_type=" + << orca_type; + } + } + } else { + // Handle potential errors + LOG_ERROR << "Failed to get KV metrics: " << TRITONSERVER_ErrorMessage(err); + TRITONSERVER_ErrorDelete(err); + } + TRITONSERVER_MetricsDelete(metrics); +} + +std::vector +MetricFamilyExtractor(const std::string& input, const std::string& metricFamily) +{ + std::vector metrics; + // Construct the regex pattern using the provided metricFamily. + + // `labelGroup` is a capturing group that captures all characters within curly + // braces, excluding line breaks. + std::string labelGroup = "(?:{(.*?)})"; + + // `valueGroup` is a capturing group that captures a number with its + // decimals if any. + std::string valueGroup = R"((\d+(?:\.\d+)?))"; + + // `patternStr` matches on lines starting with `metricFamily` then captures + // its labels if any, then (optionally) matches any whitespace, then captures + // its numeric double value. + // + // For example, `patternStr` would match on input: + // `nv_trt_llm_kv_cache_block_metrics{kv_cache_block_type="used",model="tensorrt_llm",version="1"} + // 3` + // + // with 2 capturing groups: + // 1. `kv_cache_block_type="used",model="tensorrt_llm",version="1"` + // 2. `3` + std::string patternStr = metricFamily + labelGroup + R"(?\s*)" + valueGroup; + re2::RE2 pattern(patternStr); + re2::StringPiece inputPiece(input); + + std::string labelString; + std::string metric_value; + + while (re2::RE2::FindAndConsume( + &inputPiece, pattern, &labelString, &metric_value)) { + PromMetric metric; + + // Extract labels if they exist + if (!labelString.empty()) { + // `labelPattern` captures any alphanumeric sequence that precedes an '=' + // character, then captures the following quoted character sequence. These + // groups are exahstive given the prometheus data model: + // https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels + // + // For example, calling FindAndConsume() with `labelPattern` on input: + // `kv_cache_block_type="used",model="tensorrt_llm",version="1"` + // + // matches 3 times with 2 capturing groups each: + // + // Match #1 + // 1. `kv_cache_block_type` + // 2. `used` + // + // Match #2 + // 1. `model` + // 2. `tensorrt_llm` + // + // Match #3 + // 1. `version` + // 2. `1` + re2::RE2 labelPattern(R"((\w+)=\"([^\"]*)\")"); + re2::StringPiece labelPiece(labelString); + std::string key, value; + while ( + re2::RE2::FindAndConsume(&labelPiece, labelPattern, &key, &value)) { + // Populate the metric's labels map + metric.labels[key] = value; + } + } + + // Assign the metric its value and add it to the family list + metric.value = stod(metric_value); + metrics.push_back(metric); + } + + return metrics; +} + +std::string +ExtractKVMetrics( + const std::string& prometheus_metrics, const std::string& orca_type) +{ + std::string metric_family = KV_CACHE_BLOCK_METRICS_FAMILY; + std::vector kv_cache_metrics = + MetricFamilyExtractor(prometheus_metrics, metric_family); + + double tokens_per_block = -1; + double used_blocks = -1; + double max_blocks = -1; + + for (const auto& metric : kv_cache_metrics) { + if (metric.labels.count(KV_CACHE_BLOCK_TYPE) > 0) { + std::string type = metric.labels.at(KV_CACHE_BLOCK_TYPE); + if (type == KV_CACHE_BLOCK_TYPE_TOKENS_PER) { + tokens_per_block = metric.value; + } else if (type == KV_CACHE_BLOCK_TYPE_USED) { + used_blocks = metric.value; + } else if (type == KV_CACHE_BLOCK_TYPE_MAX) { + max_blocks = metric.value; + } + } + } + + // Return early if not all kv metrics are found and set. + if (tokens_per_block < 0 || used_blocks < 0 || max_blocks < 0) { + LOG_ERROR << "One or more of the kv metrics was not found or invalid."; + return ""; + } + + // Calculate derived metrics + double kv_cache_utilization = 0; + if (max_blocks > 0) { + kv_cache_utilization = used_blocks / max_blocks; + } + double max_token_capacity = max_blocks * tokens_per_block; + + std::unordered_map + metrics; // metrics vector to pass down + metrics[KV_CACHE_UTIL_KEY] = kv_cache_utilization; + metrics[MAX_TOKEN_CAPACITY_KEY] = max_token_capacity; + + return OrcaKVMetricHeader(orca_type, metrics); +} + +std::string +OrcaKVMetricHeader( + const std::string& orca_type, + std::unordered_map metrics) +{ + // Logic to construct and format response header + std::string header_contents = ""; + const std::string named_metrics_key = NAMED_METRICS; + const std::string kv_util_key = KV_CACHE_UTIL_KEY; + const std::string max_token_key = MAX_TOKEN_CAPACITY_KEY; + + if (orca_type == "json") { + // Format the metrics according to the ORCA protocol as JSON. + triton::common::TritonJson::Value orca_metrics( + triton::common::TritonJson::ValueType::OBJECT); + triton::common::TritonJson::Value named_metrics( + orca_metrics, triton::common::TritonJson::ValueType::OBJECT); + + named_metrics.AddDouble(kv_util_key.c_str(), metrics[kv_util_key]); + named_metrics.AddUInt(max_token_key.c_str(), metrics[max_token_key]); + orca_metrics.Add(named_metrics_key.c_str(), std::move(named_metrics)); + + triton::common::TritonJson::WriteBuffer buffer; + orca_metrics.Write(&buffer); + header_contents = std::string("JSON ") + buffer.Contents(); + + } else if (orca_type == "text") { + // Format the metrics according to the ORCA protocol as Native HTTP + // (comma separated list). + const std::string prefix = named_metrics_key + "."; + + header_contents = "TEXT "; + header_contents += prefix + kv_util_key + "=" + + std::to_string(metrics[kv_util_key]) + ", "; + header_contents += + prefix + max_token_key + "=" + + std::to_string(static_cast(metrics[max_token_key])); + } else { + LOG_ERROR << "orca_type is set to an invalid type: " << orca_type; + } + + return header_contents; +} diff --git a/src/orca_http.h b/src/orca_http.h new file mode 100644 index 0000000000..ffb367bd4b --- /dev/null +++ b/src/orca_http.h @@ -0,0 +1,67 @@ +// Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of NVIDIA CORPORATION nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +// OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#pragma once + +#include +#include +#include + +#include "http_server.h" + +#define ENDPOINT_LOAD_METRICS_TYPE "endpoint-load-metrics-format" +#define ENDPOINT_LOAD_METRICS_NAME "endpoint-load-metrics" +#define KV_CACHE_BLOCK_METRICS_FAMILY "nv_trt_llm_kv_cache_block_metrics" +#define KV_CACHE_BLOCK_TYPE "kv_cache_block_type" +#define KV_CACHE_BLOCK_TYPE_TOKENS_PER "tokens_per" +#define KV_CACHE_BLOCK_TYPE_USED "used" +#define KV_CACHE_BLOCK_TYPE_MAX "max" +#define KV_CACHE_UTIL_KEY "kv_cache_utilization" +#define MAX_TOKEN_CAPACITY_KEY "max_token_capacity" +#define NAMED_METRICS "named_metrics" + +struct PromMetric { + std::unordered_map labels; + double value; +}; + +// function with logic to pull the KV-cache metrics for the inference +// response header +void SetEndpointLoadMetricsHeader( + evhtp_request_t* req, const char* orca_metric_format, + TRITONSERVER_Server* server); +// Helper function to get the KV-cache utilization metrics for the +// inference response header +std::string ExtractKVMetrics( + const std::string& prometheus_metrics, const std::string& orca_type); +// Generates a metric struct for a given family with a map of labels and a +// value +std::vector MetricFamilyExtractor( + const std::string& input, const std::string& metricFamily); +// Creates a header string in the the proper reporting format for provided +// KV-cache metrics. +std::string OrcaKVMetricHeader( + const std::string& reporting_format, + const std::unordered_map metrics); From 422527d5f009aa2b4fc5a58bee75c26d77b30c90 Mon Sep 17 00:00:00 2001 From: Indrajit Bhosale Date: Mon, 17 Mar 2025 00:02:22 -0700 Subject: [PATCH 004/230] test: Add test for ORCA (#8009) --- qa/L0_orca/orca_http_test.py | 161 ++++++++++++++++++++++++++++++++ qa/L0_orca/test.sh | 174 +++++++++++++++++++++++++++++++++++ 2 files changed, 335 insertions(+) create mode 100755 qa/L0_orca/orca_http_test.py create mode 100755 qa/L0_orca/test.sh diff --git a/qa/L0_orca/orca_http_test.py b/qa/L0_orca/orca_http_test.py new file mode 100755 index 0000000000..936fb97ae1 --- /dev/null +++ b/qa/L0_orca/orca_http_test.py @@ -0,0 +1,161 @@ +#!/usr/bin/python3 +# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import argparse +import json +import sys + +import requests + + +# To run the test, have tritonserver running and run this script with the endpoint as a flag. +# +# Example: +# ``` +# python3 orca_header_test.py http://localhost:8000/v2/models/ensemble/generate +# ``` +def get_endpoint_header(url, data, request_header=None): + """ + Sends a POST request to the given URL with the provided data and returns the value of the "endpoint-load-metrics" header, + or None if the request fails. + """ + HEADER_KEY = "endpoint-load-metrics" + try: + response = None + if request_header: + response = requests.post(url, json=data, headers=request_header) + else: + response = requests.post(url, json=data) + response.raise_for_status() + return response.headers.get(HEADER_KEY, "") + except requests.exceptions.RequestException as e: + print(f"Error making request: {e}") + return None + + +def parse_header_data(header, orca_format): + """ + Parses the header data into a dictionary based on the given format. + """ + METRIC_KEY = "named_metrics" + try: + if orca_format == "json": + # Parse the header in JSON format + data = json.loads(header.replace("JSON ", "")) + if METRIC_KEY in data: + return data[METRIC_KEY] + else: + print(f"No key '{METRIC_KEY}' in header data: {data}") + return None + elif orca_format == "text": + # Parse the header in TEXT format + data = {} + for key_value_pair in header.replace("TEXT ", "").split(", "): + key, value = key_value_pair.split("=") + if "." in key: + prefix, nested_key = key.split(".", 1) + if prefix == METRIC_KEY: + data[nested_key] = float(value) + if not data: + print(f"Could not parse any keys from header: {header}") + return None + return data + else: + print(f"Invalid ORCA format: {orca_format}") + return None + except (json.JSONDecodeError, ValueError, KeyError): + print("Error: Invalid data in the header.") + return None + + +def check_for_keys(data, desired_keys, orca_format): + """ + Checks if all desired keys are present in the given data dictionary. + """ + if all(key in data for key in desired_keys): + print( + f"ORCA header present in {orca_format} format with kv_cache_utilization: {[f'{k}: {data[k]}' for k in desired_keys]}" + ) + return True + else: + print(f"Missing keys in header: {', '.join(set(desired_keys) - set(data))}") + return False + + +def request_header(orca_format): + return {"endpoint-load-metrics-format": orca_format} if orca_format else None + + +def test_header_type(url, data, orca_format): + req_header = request_header(orca_format) + response_header = get_endpoint_header(args.url, TEST_DATA, req_header) + + desired_keys = { + "kv_cache_utilization", + "max_token_capacity", + } # Just the keys, no need to initialize with None + + if response_header is None: + print(f"Request to endpoint: '{args.url}' failed.") + return False + elif response_header == "": + if orca_format: + print( + f"response header empty, endpoint-load-metrics-format={orca_format} is not a valid ORCA metric format" + ) + return False + else: + # No request header set <=> no response header. Intended behavior. + print(f"response header empty, endpoint-load-metrics-format is not set") + return True + + data = parse_header_data(response_header, orca_format) + if data: + return check_for_keys(data, desired_keys, orca_format) + else: + print(f"Unexpected response header value: {response_header}") + return False + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Make a POST request to generate endpoint to test the ORCA metrics header." + ) + parser.add_argument("url", help="The model URL to send the request to.") + args = parser.parse_args() + TEST_DATA = json.loads( + '{"text_input": "hello world", "max_tokens": 20, "bad_words": "", "stop_words": ""}' + ) + passed = True + + for format in ["json", "text", None]: + print("Checking response header for ORCA format:", format) + if not test_header_type(args.url, TEST_DATA, format): + print("FAIL on format:", format) + passed = False + + sys.exit(0 if passed else 1) diff --git a/qa/L0_orca/test.sh b/qa/L0_orca/test.sh new file mode 100755 index 0000000000..6069a75048 --- /dev/null +++ b/qa/L0_orca/test.sh @@ -0,0 +1,174 @@ +#!/bin/bash +# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +RET=0 +BASE_DIR=$(pwd) +NUM_GPUS=${NUM_GPUS:=1} +TENSORRTLLM_BACKEND_REPO_TAG=${TENSORRTLLM_BACKEND_REPO_TAG:="main"} +TRITON_REPO_ORG=${TRITON_REPO_ORG:="https://github.com/triton-inference-server"} +TRT_ROOT="/usr/local/tensorrt" + +MODEL_NAME="gpt2_tensorrt_llm" +NAME="tensorrt_llm_benchmarking_test" +MODEL_REPOSITORY="$(pwd)/triton_model_repo" +TENSORRTLLM_BACKEND_DIR="/workspace/tensorrtllm_backend" +GPT_DIR="$TENSORRTLLM_BACKEND_DIR/tensorrt_llm/examples/gpt" +TOKENIZER_DIR="$GPT_DIR/gpt2" +ENGINES_DIR="${BASE_DIR}/engines/inflight_batcher_llm/${NUM_GPUS}-gpu" +TRITON_DIR=${TRITON_DIR:="/opt/tritonserver"} +SERVER=${TRITON_DIR}/bin/tritonserver +BACKEND_DIR=${TRITON_DIR}/backends +SERVER_LOG="${NAME}_server.log" +SERVER_TIMEOUT=${SERVER_TIMEOUT:=120} +CLIENT_PY=${BASE_DIR}/orca_http_test.py +CLIENT_LOG="${NAME}_orca_http_test.log" +source ../common/util.sh + +function prepare_model_repository { + rm -rf ${MODEL_REPOSITORY} && mkdir ${MODEL_REPOSITORY} + cp -r ${TENSORRTLLM_BACKEND_DIR}/all_models/inflight_batcher_llm/* ${MODEL_REPOSITORY} + rm -rf ${MODEL_REPOSITORY}/tensorrt_llm_bls + mv "${MODEL_REPOSITORY}/ensemble" "${MODEL_REPOSITORY}/${MODEL_NAME}" + + replace_config_tags "model_version: -1" "model_version: 1" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" + replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" + replace_config_tags 'name: "ensemble"' "name: \"$MODEL_NAME\"" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" + replace_config_tags '${logits_datatype}' "TYPE_FP32" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" + + replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" + replace_config_tags '${preprocessing_instance_count}' '1' "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" + replace_config_tags '${tokenizer_dir}' "${TOKENIZER_DIR}/" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" + replace_config_tags '${logits_datatype}' "TYPE_FP32" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" + replace_config_tags '${max_queue_delay_microseconds}' "1000000" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" + replace_config_tags '${max_queue_size}' "0" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" + + replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" + replace_config_tags '${postprocessing_instance_count}' '1' "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" + replace_config_tags '${tokenizer_dir}' "${TOKENIZER_DIR}/" "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" + replace_config_tags '${logits_datatype}' "TYPE_FP32" "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" + + replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${decoupled_mode}' 'true' "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${max_queue_delay_microseconds}' "1000000" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${batching_strategy}' 'inflight_fused_batching' "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${engine_dir}' "${ENGINES_DIR}" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${triton_backend}' "tensorrtllm" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${max_queue_size}' "0" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${logits_datatype}' "TYPE_FP32" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${encoder_input_features_data_type}' "TYPE_FP32" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" +} + +# Wait until server health endpoint shows ready. Sets WAIT_RET to 0 on +# success, 1 on failure +function wait_for_server_ready() { + local wait_time_secs="${1:-30}" + shift + local spids=("$@") + + WAIT_RET=0 + + for _ in $(seq "$wait_time_secs"); do + for pid in "${spids[@]}"; do + if ! kill -0 "$pid" >/dev/null 2>&1; then + echo "=== Server not running." + WAIT_RET=1 + return + fi + done + + sleep 1 + + if curl -s --fail localhost:8000/v2/health/ready && + curl -s --fail -w "%{http_code}" -o /dev/null -d '{"log_verbose_level":1}' localhost:8000/v2/logging; then + return + fi + done + + echo "=== Timeout $wait_time_secs secs. Server not ready." + WAIT_RET=1 +} + +function run_server { + python3 ${TENSORRTLLM_BACKEND_DIR}/scripts/launch_triton_server.py --world_size="${NUM_GPUS}" --model_repo="${MODEL_REPOSITORY}" >${SERVER_LOG} 2>&1 & + sleep 2 # allow time to obtain the pid(s) + # Read PIDs into an array, trimming whitespaces + readarray -t SERVER_PID < <(pgrep "tritonserver") + + wait_for_server_ready ${SERVER_TIMEOUT} "${SERVER_PID[@]}" + if [ "$WAIT_RET" != "0" ]; then + # Cleanup + kill "${SERVER_PID[@]}" >/dev/null 2>&1 || true + echo -e "\n***\n*** Failed to start $SERVER\n***" + cat $SERVER_LOG + exit 1 + fi +} + +function kill_server { + pgrep tritonserver | xargs kill -SIGINT + for pid in "${SERVER_PID[@]}"; do + echo "Waiting for proc ${pid} to terminate..." + while kill -0 $pid >/dev/null 2>&1; do + sleep 1 + done + done +} + +clone_tensorrt_llm_backend_repo +build_gpt2_base_model +build_gpt2_tensorrt_engine +prepare_model_repository + +set +e +run_server + +if [ "$SERVER_PID" == "0" ]; then + echo -e "\n***\n*** Failed to start $SERVER\n***" + cat $SERVER_LOG + exit 1 +fi + +RET=0 + +python3 $CLIENT_PY "http://localhost:8000/v2/models/${MODEL_NAME}/generate" >>$CLIENT_LOG 2>&1 + +if [ $? -ne 0 ]; then + echo "Failed: Client test had a non-zero return code." + RET=1 +fi + +if [ $RET -eq 0 ]; then + echo -e "\n***\n*** ORCA Test Passed\n***" +else + cat $SERVER_LOG + cat $CLIENT_LOG + echo -e "\n***\n*** ORCA Test FAILED\n***" +fi + +kill_server +set -e +exit $RET From 6afaed7c4f0dbd83d59627a7f546baaf527fb856 Mon Sep 17 00:00:00 2001 From: Indrajit Bhosale Date: Mon, 17 Mar 2025 11:08:16 -0700 Subject: [PATCH 005/230] Remove extra file from ORCA commit (#8075) --- qa/common/orca_header_test.py | 167 ---------------------------------- 1 file changed, 167 deletions(-) delete mode 100755 qa/common/orca_header_test.py diff --git a/qa/common/orca_header_test.py b/qa/common/orca_header_test.py deleted file mode 100755 index 660b025556..0000000000 --- a/qa/common/orca_header_test.py +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/python3 -# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import sys - -sys.path.append("../common") - -import argparse -import json - -import requests - - -# To run the test, have tritonserver running and run this script with the endpoint as a flag. -# -# Example: -# ``` -# python3 orca_header_test.py http://localhost:8000/v2/models/ensemble/generate -# ``` -def get_endpoint_header(url, data, request_header=None): - """ - Sends a POST request to the given URL with the provided data and returns the value of the "endpoint-load-metrics" header, - or None if the request fails. - """ - HEADER_KEY = "endpoint-load-metrics" - try: - response = None - if request_header: - response = requests.post(url, json=data, headers=request_header) - else: - response = requests.post(url, json=data) - response.raise_for_status() - return response.headers.get(HEADER_KEY, "") - except requests.exceptions.RequestException as e: - print(f"Error making request: {e}") - return None - - -def parse_header_data(header, orca_format): - """ - Parses the header data into a dictionary based on the given format. - """ - METRIC_KEY = "named_metrics" - try: - if orca_format == "json": - # Parse the header in JSON format - data = json.loads(header.replace("JSON ", "")) - if METRIC_KEY in data: - return data[METRIC_KEY] - else: - print(f"No key '{METRIC_KEY}' in header data: {data}") - return None - elif orca_format == "text": - # Parse the header in TEXT format - data = {} - for key_value_pair in header.replace("TEXT ", "").split(", "): - key, value = key_value_pair.split("=") - if "." in key: - prefix, nested_key = key.split(".", 1) - if prefix == METRIC_KEY: - data[nested_key] = float(value) - if not data: - print(f"Could not parse any keys from header: {header}") - return None - return data - else: - print(f"Invalid ORCA format: {orca_format}") - return None - except (json.JSONDecodeError, ValueError, KeyError): - print("Error: Invalid data in the header.") - return None - - -def check_for_keys(data, desired_keys, orca_format): - """ - Checks if all desired keys are present in the given data dictionary. - """ - if all(key in data for key in desired_keys): - print( - "ORCA header present in ", - orca_format, - "format with" "kv_cache_utilization:", - [k + ": " + str(data[k]) for k in desired_keys], - ) - return True - else: - print(f"Missing keys in header: {', '.join(set(desired_keys) - set(data))}") - return False - - -def request_header(orca_format): - return {"endpoint-load-metrics-format": orca_format} if orca_format else None - - -def test_header_type(url, data, orca_format): - req_header = request_header(orca_format) - response_header = get_endpoint_header(args.url, TEST_DATA, req_header) - - desired_keys = { - "kv_cache_utilization", - "max_token_capacity", - } # Just the keys, no need to initialize with None - - if response_header is None: - print(f"Request to endpoint: '{args.url}' failed.") - return False - elif response_header == "": - if orca_format: - print( - f"response header empty, endpoint-load-metrics-format={orca_format} is not a valid ORCA metric format" - ) - return False - else: - # No request header set <=> no response header. Intended behavior. - print(f"response header empty, endpoint-load-metrics-format is not set") - return True - - data = parse_header_data(response_header, orca_format) - if data: - return check_for_keys(data, desired_keys, orca_format) - else: - print(f"Unexpected response header value: {response_header}") - return False - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Make a POST request to generate endpoint to test the ORCA metrics header." - ) - parser.add_argument("url", help="The model URL to send the request to.") - args = parser.parse_args() - TEST_DATA = json.loads( - '{"text_input": "hello world", "max_tokens": 20, "bad_words": "", "stop_words": ""}' - ) - passed = True - - for format in ["json", "text", None]: - print("Checking response header for ORCA format:", format) - if not test_header_type(args.url, TEST_DATA, format): - print("FAIL on format:", format) - passed = False - - sys.exit(0 if passed else 1) From 03dbf31d9d74f0e055e125c061694cc69569495e Mon Sep 17 00:00:00 2001 From: Ziqi Fan Date: Tue, 25 Mar 2025 14:47:38 -0700 Subject: [PATCH 006/230] docs: rename userguide AI Agents section to Features | add spec decoding to Features (#8099) --- docs/contents.rst | 7 +-- docs/llm_features/speculative_decoding.rst | 54 +++++++++++++++++++ .../speculative_decoding_by_backend_type.rst | 39 ++++++++++++++ 3 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 docs/llm_features/speculative_decoding.rst create mode 100644 docs/llm_features/speculative_decoding_by_backend_type.rst diff --git a/docs/contents.rst b/docs/contents.rst index 555c433d85..dfff933e31 100644 --- a/docs/contents.rst +++ b/docs/contents.rst @@ -50,10 +50,11 @@ .. toctree:: :hidden: - :caption: AI Agents + :caption: LLM Features - Constrained Decoding <../tutorials/AI_Agents_Guide/Constrained_Decoding/README.md> - Function Calling <../tutorials/AI_Agents_Guide/Function_Calling/README.md> + Constrained Decoding <../tutorials/Feature_Guide/Constrained_Decoding/README.md> + Function Calling <../tutorials/Feature_Guide/Function_Calling/README.md> + llm_features/speculative_decoding_by_backend_type .. toctree:: :hidden: diff --git a/docs/llm_features/speculative_decoding.rst b/docs/llm_features/speculative_decoding.rst new file mode 100644 index 0000000000..debbcf52ae --- /dev/null +++ b/docs/llm_features/speculative_decoding.rst @@ -0,0 +1,54 @@ +.. +.. Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. +.. Redistribution and use in source and binary forms, with or without +.. modification, are permitted provided that the following conditions +.. are met: +.. * Redistributions of source code must retain the above copyright +.. notice, this list of conditions and the following disclaimer. +.. * Redistributions in binary form must reproduce the above copyright +.. notice, this list of conditions and the following disclaimer in the +.. documentation and/or other materials provided with the distribution. +.. * Neither the name of NVIDIA CORPORATION nor the names of its +.. contributors may be used to endorse or promote products derived +.. from this software without specific prior written permission. +.. +.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +.. EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +.. PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +.. CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +.. EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +.. PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +.. PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +.. OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +.. (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +.. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +.. raw:: html + + +About Speculative Decoding +========================= +Speculative Decoding (also referred to as Speculative Sampling) is a set of techniques designed +to allow generation of more than one token per forward pass iteration. This can lead to a reduction +in the average per-token latency in situations where the GPU is underutilized due to small batch sizes. + +Speculative decoding involves predicting a sequence of future tokens, referred to as draft tokens, +using a method that is substantially more efficient than repeatedly executing the target Large Language +Model (LLM). These draft tokens are then collectively validated by processing them through the target LLM +in a single forward pass. The underlying assumptions are twofold: + +1. processing multiple draft tokens concurrently will be as rapid as processing a single token +2. multiple draft tokens will be validated successfully over the course of the full generation + +If the first assumption holds true, the latency of speculative decoding will no worse than the standard +approach. If the second holds, output token generation advances by statistically more than one token per +forward pass. The combination of both these allows speculative decoding to result in reduced latency. + +Performance Improvements +======================== +It's important to note that the effectiveness of speculative decoding techniques is highly dependent +on the specific task at hand. For instance, forecasting subsequent tokens in a code-completion scenario +may prove simpler than generating a summary for an article. `Spec-Bench `__ +shows the performance of different speculative decoding approaches on different tasks. \ No newline at end of file diff --git a/docs/llm_features/speculative_decoding_by_backend_type.rst b/docs/llm_features/speculative_decoding_by_backend_type.rst new file mode 100644 index 0000000000..a61f625626 --- /dev/null +++ b/docs/llm_features/speculative_decoding_by_backend_type.rst @@ -0,0 +1,39 @@ +.. +.. Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. +.. Redistribution and use in source and binary forms, with or without +.. modification, are permitted provided that the following conditions +.. are met: +.. * Redistributions of source code must retain the above copyright +.. notice, this list of conditions and the following disclaimer. +.. * Redistributions in binary form must reproduce the above copyright +.. notice, this list of conditions and the following disclaimer in the +.. documentation and/or other materials provided with the distribution. +.. * Neither the name of NVIDIA CORPORATION nor the names of its +.. contributors may be used to endorse or promote products derived +.. from this software without specific prior written permission. +.. +.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +.. EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +.. PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +.. CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +.. EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +.. PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +.. PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +.. OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +.. (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +.. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#### +Speculative Decoding +#### + +.. include:: speculative_decoding.rst + +.. toctree:: + :maxdepth: 1 + :hidden: + + TRT-LLM <../tutorials/Feature_Guide/Speculative_Decoding/TRT-LLM/README.md> + vLLM <../tutorials/Feature_Guide/Speculative_Decoding/vLLM/README.md> \ No newline at end of file From fd19783faa288a43522ea1a1a91d4e2b6641e598 Mon Sep 17 00:00:00 2001 From: Ziqi Fan Date: Wed, 26 Mar 2025 10:21:40 -0700 Subject: [PATCH 007/230] docs: change server README to only load densenet_onnx since tf model cannot load after 25.03 (#8089) --- README.md | 5 ++--- docs/examples/fetch_models.sh | 9 +-------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f62a1a176e..3ec027aa6c 100644 --- a/README.md +++ b/README.md @@ -89,12 +89,11 @@ cd server/docs/examples ./fetch_models.sh # Step 2: Launch triton from the NGC Triton container -docker run --gpus=1 --rm --net=host -v ${PWD}/model_repository:/models nvcr.io/nvidia/tritonserver:25.02-py3 tritonserver --model-repository=/models +docker run --gpus=1 --rm --net=host -v ${PWD}/model_repository:/models nvcr.io/nvidia/tritonserver:25.02-py3 tritonserver --model-repository=/models --model-control-mode explicit --load-model densenet_onnx # Step 3: Sending an Inference Request # In a separate console, launch the image_client example from the NGC Triton SDK container -docker run -it --rm --net=host nvcr.io/nvidia/tritonserver:25.02-py3-sdk -/workspace/install/bin/image_client -m densenet_onnx -c 3 -s INCEPTION /workspace/images/mug.jpg +docker run -it --rm --net=host nvcr.io/nvidia/tritonserver:25.02-py3-sdk /workspace/install/bin/image_client -m densenet_onnx -c 3 -s INCEPTION /workspace/images/mug.jpg # Inference should return the following Image '/workspace/images/mug.jpg': diff --git a/docs/examples/fetch_models.sh b/docs/examples/fetch_models.sh index f5aaed85aa..f357a6684e 100755 --- a/docs/examples/fetch_models.sh +++ b/docs/examples/fetch_models.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2018, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2018-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -27,13 +27,6 @@ set -ex -# TensorFlow inception -mkdir -p model_repository/inception_graphdef/1 -wget -O /tmp/inception_v3_2016_08_28_frozen.pb.tar.gz \ - https://storage.googleapis.com/download.tensorflow.org/models/inception_v3_2016_08_28_frozen.pb.tar.gz -(cd /tmp && tar xzf inception_v3_2016_08_28_frozen.pb.tar.gz) -mv /tmp/inception_v3_2016_08_28_frozen.pb model_repository/inception_graphdef/1/model.graphdef - # ONNX densenet mkdir -p model_repository/densenet_onnx/1 wget -O model_repository/densenet_onnx/1/model.onnx \ From 5fd6bc48bb056527d66662d6e1688e8a3af1926e Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Fri, 4 Apr 2025 16:49:01 -0400 Subject: [PATCH 008/230] feat: Configurable grpc infer thread count (#8061) --- .../inference_protocols.md | 10 ++++- qa/L0_sequence_stress/test.sh | 41 ++++++++++++++++++- src/command_line_parser.cc | 14 +++++++ src/grpc/grpc_server.cc | 6 +-- src/grpc/grpc_server.h | 3 ++ 5 files changed, 69 insertions(+), 5 deletions(-) diff --git a/docs/customization_guide/inference_protocols.md b/docs/customization_guide/inference_protocols.md index dea3e0459e..053ef13402 100644 --- a/docs/customization_guide/inference_protocols.md +++ b/docs/customization_guide/inference_protocols.md @@ -1,5 +1,5 @@ -# [Triton Inference Server Release 25.02](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-25-02.html#rel-25-02) +# [Triton Inference Server Release 25.03](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-25-03.html#rel-25-03) -The Triton Inference Server container image, release 25.02, is available +The Triton Inference Server container image, release 25.03, is available on [NGC](https://ngc.nvidia.com/catalog/containers/nvidia:tritonserver) and is open source on [GitHub](https://github.com/triton-inference-server/server). Release notes can diff --git a/python/openai/README.md b/python/openai/README.md index c5a3cb4d0a..53a9f461f4 100644 --- a/python/openai/README.md +++ b/python/openai/README.md @@ -51,7 +51,7 @@ docker run -it --net=host --gpus all --rm \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ -e HF_TOKEN \ - nvcr.io/nvidia/tritonserver:25.02-vllm-python-py3 + nvcr.io/nvidia/tritonserver:25.03-vllm-python-py3 ``` 2. Launch the OpenAI-compatible Triton Inference Server: diff --git a/qa/L0_backend_config/test.sh b/qa/L0_backend_config/test.sh index dad586883b..16625746b3 100755 --- a/qa/L0_backend_config/test.sh +++ b/qa/L0_backend_config/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2022-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -57,7 +57,7 @@ fi rm -rf ./models/ mkdir -p ./models/no_config -cp -r /data/inferenceserver/${REPO_VERSION}/qa_model_repository/savedmodel_float32_float32_float32/1 ./models/no_config/ +cp -r /data/inferenceserver/${REPO_VERSION}/qa_model_repository/onnx_float32_float32_float32/1 ./models/no_config/ SERVER=/opt/tritonserver/bin/tritonserver @@ -73,14 +73,12 @@ COMMON_ARGS="--model-repository=`pwd`/models --strict-model-config=false --log-v NEGATIVE_PARSE_ARGS=("--backend-config=,default-max-batch-size=3 $COMMON_ARGS" \ "--backend-config=default-max-batch-size= $COMMON_ARGS" \ "--backend-config=default-max-batch-size $COMMON_ARGS" \ - "--backend-config=tensorflow,default-max-batch-size= $COMMON_ARGS" \ - "--backend-config=tensorflow,default-max-batch-size $COMMON_ARGS" \ ) POSITIVE_DEFAULT_ARGS=$COMMON_ARGS -POSITIVE_TEST_ARGS=("--backend-config=tensorflow,default-max-batch-size=5 $COMMON_ARGS" \ +POSITIVE_TEST_ARGS=("--backend-config=default-max-batch-size=5 $COMMON_ARGS" \ "--backend-config=default-max-batch-size=6 $COMMON_ARGS" \ - "--backend-config=default-max-batch-size=7 --backend-config=tensorflow,default-max-batch-size=8 $COMMON_ARGS" \ + "--backend-config=default-max-batch-size=7 --backend-config=default-max-batch-size=8 $COMMON_ARGS" \ ) # These integers correspond to the expected default-max-batch-size which gets set @@ -185,74 +183,6 @@ function save_model_config() { fi } -# Tensorflow 1: Batching ON -rm -rf ./models/ -mkdir -p ./models/no_config -cp -r /data/inferenceserver/${REPO_VERSION}/qa_model_repository/savedmodel_float32_float32_float32/1 ./models/no_config/ - -SERVER_ARGS="--backend-config=tensorflow,default-max-batch-size=5 $COMMON_ARGS" -SERVER_LOG=$SERVER_LOG_BASE.backend_config_tensorflow_batch_5.log -run_server - -TRIAL=tensorflow_batching_on -if [ "$SERVER_PID" == "0" ]; then - echo -e "*** FAILED: Server failed to start $SERVER\n" - RET=1 -else - save_model_config - - # Assert the max-batch-size is the command line value - MAX_BATCH_LOG_LINE=$(grep -a "\"max_batch_size\":5" $TRIAL.out) - if [ "$MAX_BATCH_LOG_LINE" == "" ]; then - cat $TRIAL.out - echo "*** FAILED: Expected max batch size to be 5 but found: $MAX_BATCH_LOG_LINE\n" - RET=1 - fi - - # Assert we are also turning on the dynamic_batcher - DYNAMIC_BATCHING_LOG_LINE=$(grep -a "Starting dynamic-batcher thread" $SERVER_LOG) - if [ "$DYNAMIC_BATCHING_LOG_LINE" == "" ]; then - echo "*** FAILED: Expected dynamic batching to be set in model config but was not found\n" - RET=1 - fi - - kill $SERVER_PID - wait $SERVER_PID - -fi - -# Tensorflow 1: Batching OFF -SERVER_ARGS="--backend-config=tensorflow,default-max-batch-size=0 $COMMON_ARGS" -SERVER_LOG=$SERVER_LOG_BASE.backend_config_tensorflow_batch_0.log -run_server - -TRIAL=tensorflow_batching_off -if [ "$SERVER_PID" == "0" ]; then - echo -e "*** FAILED: Server failed to start $SERVER\n" - RET=1 - -else - save_model_config - - # Assert the max-batch-size is 0 in the case batching is supported - # in the model but not in the config. - MAX_BATCH_LOG_LINE=$(grep -a "\"max_batch_size\":0" $TRIAL.out) - if [ "$MAX_BATCH_LOG_LINE" == "" ]; then - echo "*** FAILED: Expected max batch size to be 0 but found: $MAX_BATCH_LOG_LINE\n" - RET=1 - fi - - # Assert batching disabled - if [ "$(grep -a -E '\"dynamic_batching\": \{}' $SERVER_LOG)" != "" ]; then - echo "*** FAILED: Found dynamic batching enabled in configuration when none expected.\n" - RET=1 - fi - - kill $SERVER_PID - wait $SERVER_PID - -fi - # Onnxruntime: Batching ON rm -rf ./models/ mkdir -p ./models/no_config diff --git a/qa/L0_backend_python/env/test.sh b/qa/L0_backend_python/env/test.sh index 0279d7984c..4d9de30a56 100755 --- a/qa/L0_backend_python/env/test.sh +++ b/qa/L0_backend_python/env/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2021-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -41,77 +41,6 @@ rm -rf *.tar.gz install_build_deps install_conda -# Tensorflow 2.1.0 only works with Python 3.4 - 3.7. Successful execution of -# the Python model indicates that the environment has been setup correctly. -# Create a model with python 3.7 version -export PY_VERSION="3.7" -create_conda_env "3.7" "python-3-7" -conda install numpy=1.20.1 -y -conda install tensorflow=2.1.0 -y -conda install -c conda-forge libstdcxx-ng=14 -y - -PY37_VERSION_STRING="Python version is 3.7, NumPy version is 1.20.1, and Tensorflow version is 2.1.0" -create_python_backend_stub -conda-pack -o python3.7.tar.gz -path_to_conda_pack=`pwd`/python3.7.tar.gz -mkdir -p models/python_3_7/1/ -cp ../../python_models/python_version/config.pbtxt ./models/python_3_7 -(cd models/python_3_7 && \ - sed -i "s/^name:.*/name: \"python_3_7\"/" config.pbtxt && \ - echo "parameters: {key: \"EXECUTION_ENV_PATH\", value: {string_value: \"$path_to_conda_pack\"}}">> config.pbtxt) -cp ../../python_models/python_version/model.py ./models/python_3_7/1/ -cp python_backend/builddir/triton_python_backend_stub ./models/python_3_7 -conda deactivate - -# Use python-3-7 without conda pack -# Create a model with python 3.7 version and numpy 1.20.3 to distinguish from -# previous test. -# Tensorflow 2.1.0 only works with Python 3.4 - 3.7. Successful execution of -# the Python model indicates that the environment has been setup correctly. -export PY_VERSION="3.7.1" -path_to_conda_pack="$PWD/python-3-7-1" -create_conda_env_with_specified_path "3.7" $path_to_conda_pack -conda install numpy=1.20.3 -y -conda install tensorflow=2.1.0 -y -conda install -c conda-forge libstdcxx-ng=14 -y - -PY37_1_VERSION_STRING="Python version is 3.7, NumPy version is 1.20.3, and Tensorflow version is 2.1.0" -create_python_backend_stub -mkdir -p models/python_3_7_1/1/ -cp ../../python_models/python_version/config.pbtxt ./models/python_3_7_1 -(cd models/python_3_7_1 && \ - sed -i "s/^name:.*/name: \"python_3_7_1\"/" config.pbtxt && \ - echo "parameters: {key: \"EXECUTION_ENV_PATH\", value: {string_value: \"$path_to_conda_pack\"}}">> config.pbtxt) -cp ../../python_models/python_version/model.py ./models/python_3_7_1/1/ -# Copy activate script to folder -cp $path_to_conda_pack/lib/python3.7/site-packages/conda_pack/scripts/posix/activate $path_to_conda_pack/bin/. -cp python_backend/builddir/triton_python_backend_stub ./models/python_3_7_1 -conda deactivate - -# Create a model with python 3.6 version -# Tensorflow 2.1.0 only works with Python 3.4 - 3.7. Successful execution of -# the Python model indicates that the environment has been setup correctly. -export PY_VERSION="3.6" -create_conda_env "3.6" "python-3-6" -conda install -c conda-forge libstdcxx-ng=14 -y -conda install numpy=1.18.1 -y -conda install tensorflow=2.1.0 -y -PY36_VERSION_STRING="Python version is 3.6, NumPy version is 1.18.1, and Tensorflow version is 2.1.0" -conda-pack -o python3.6.tar.gz - -# Test relative execution env path -path_to_conda_pack='$$TRITON_MODEL_DIRECTORY/python_3_6_environment.tar.gz' -create_python_backend_stub -mkdir -p models/python_3_6/1/ -cp ../../python_models/python_version/config.pbtxt ./models/python_3_6 -cp python3.6.tar.gz models/python_3_6/python_3_6_environment.tar.gz -(cd models/python_3_6 && \ - sed -i "s/^name:.*/name: \"python_3_6\"/" config.pbtxt && \ - echo "parameters: {key: \"EXECUTION_ENV_PATH\", value: {string_value: \"$path_to_conda_pack\"}}" >> config.pbtxt) -cp ../../python_models/python_version/model.py ./models/python_3_6/1/ -cp python_backend/builddir/triton_python_backend_stub ./models/python_3_6 -conda deactivate - # Test conda env without custom Python backend stub This environment should # always use the default Python version shipped in the container. For Ubuntu # 24.04 it is Python 3.12, for Ubuntu 22.04 is Python 3.10 and for Ubuntu 20.04 @@ -119,13 +48,13 @@ conda deactivate path_to_conda_pack='$$TRITON_MODEL_DIRECTORY/python_3_12_environment.tar.gz' create_conda_env "3.12" "python-3-12" conda install -c conda-forge libstdcxx-ng=14 -y -TF_VERSION="2.16.2" +TORCH_VERSION="2.6.0" conda install numpy=1.26.4 -y if [ $TRITON_RHEL -eq 1 ]; then - TF_VERSION="2.17.0" + TORCH_VERISON="2.17.0" fi -conda install tensorflow=${TF_VERSION} -y -PY312_VERSION_STRING="Python version is 3.12, NumPy version is 1.26.4, and Tensorflow version is ${TF_VERSION}" +conda install torch=${TORCH_VERSION} -y +PY312_VERSION_STRING="Python version is 3.12, NumPy version is 1.26.4, and PyTorch version is ${TORCH_VERISON}" conda pack -o python3.12.tar.gz mkdir -p models/python_3_12/1/ cp ../../python_models/python_version/config.pbtxt ./models/python_3_12 @@ -147,7 +76,7 @@ fi kill_server set +e -for EXPECTED_VERSION_STRING in "$PY36_VERSION_STRING" "$PY37_VERSION_STRING" "$PY37_1_VERSION_STRING" "$PY312_VERSION_STRING"; do +for EXPECTED_VERSION_STRING in "$PY312_VERSION_STRING"; do grep "$EXPECTED_VERSION_STRING" $SERVER_LOG if [ $? -ne 0 ]; then cat $SERVER_LOG diff --git a/qa/L0_backend_python/setup_python_enviroment.sh b/qa/L0_backend_python/setup_python_enviroment.sh index c84c7f5ec8..c3d0a0aed7 100755 --- a/qa/L0_backend_python/setup_python_enviroment.sh +++ b/qa/L0_backend_python/setup_python_enviroment.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -50,84 +50,16 @@ install_conda # Test other python versions conda update -n base -c defaults conda -y -# Create a model with python 3.8 version -# Successful execution of the Python model indicates that the environment has -# been setup correctly. -if [ ${PYTHON_ENV_VERSION} = "8" ]; then - create_conda_env "3.8" "python-3-8" - conda install -c conda-forge libstdcxx-ng=14 -y - conda install numpy=1.23.4 -y - conda install tensorflow=2.10.0 -y - EXPECTED_VERSION_STRING="Python version is 3.8, NumPy version is 1.23.4, and Tensorflow version is 2.10.0" - create_python_backend_stub - conda-pack -o python3.8.tar.gz - path_to_conda_pack="$PWD/python-3-8" - mkdir -p $path_to_conda_pack - tar -xzf python3.8.tar.gz -C $path_to_conda_pack - mkdir -p models/python_3_8/1/ - cp ../python_models/python_version/config.pbtxt ./models/python_3_8 - (cd models/python_3_8 && \ - sed -i "s/^name:.*/name: \"python_3_8\"/" config.pbtxt && \ - echo "parameters: {key: \"EXECUTION_ENV_PATH\", value: {string_value: \"$path_to_conda_pack\"}}">> config.pbtxt) - cp ../python_models/python_version/model.py ./models/python_3_8/1/ - cp python_backend/builddir/triton_python_backend_stub ./models/python_3_8 -fi - -# Create a model with python 3.9 version -# Successful execution of the Python model indicates that the environment has -# been setup correctly. -if [ ${PYTHON_ENV_VERSION} = "9" ]; then - create_conda_env "3.9" "python-3-9" - conda install -c conda-forge libstdcxx-ng=14 -y - conda install numpy=1.23.4 -y - conda install tensorflow=2.10.0 -y - EXPECTED_VERSION_STRING="Python version is 3.9, NumPy version is 1.23.4, and Tensorflow version is 2.10.0" - create_python_backend_stub - conda-pack -o python3.9.tar.gz - path_to_conda_pack="$PWD/python-3-9" - mkdir -p $path_to_conda_pack - tar -xzf python3.9.tar.gz -C $path_to_conda_pack - mkdir -p models/python_3_9/1/ - cp ../python_models/python_version/config.pbtxt ./models/python_3_9 - (cd models/python_3_9 && \ - sed -i "s/^name:.*/name: \"python_3_9\"/" config.pbtxt && \ - echo "parameters: {key: \"EXECUTION_ENV_PATH\", value: {string_value: \"$path_to_conda_pack\"}}">> config.pbtxt) - cp ../python_models/python_version/model.py ./models/python_3_9/1/ - cp python_backend/builddir/triton_python_backend_stub ./models/python_3_9 -fi - -# Create a model with python 3.10 version -# Successful execution of the Python model indicates that the environment has -# been setup correctly. -if [ ${PYTHON_ENV_VERSION} = "10" ]; then - create_conda_env "3.10" "python-3-10" - conda install -c conda-forge libstdcxx-ng=14 -y - conda install tensorflow=2.10.0 -y - conda install numpy=1.23.4 -y - EXPECTED_VERSION_STRING="Python version is 3.10, NumPy version is 1.23.4, and Tensorflow version is 2.10.0" - create_python_backend_stub - conda-pack -o python3.10.tar.gz - path_to_conda_pack="$PWD/python-3-10" - mkdir -p $path_to_conda_pack - tar -xzf python3.10.tar.gz -C $path_to_conda_pack - mkdir -p models/python_3_10/1/ - cp ../python_models/python_version/config.pbtxt ./models/python_3_10 - (cd models/python_3_10 && \ - sed -i "s/^name:.*/name: \"python_3_10\"/" config.pbtxt && \ - echo "parameters: {key: \"EXECUTION_ENV_PATH\", value: {string_value: \"$path_to_conda_pack\"}}">> config.pbtxt) - cp ../python_models/python_version/model.py ./models/python_3_10/1/ - cp python_backend/builddir/triton_python_backend_stub ./models/python_3_10 -fi # Create a model with python 3.11 version # Successful execution of the Python model indicates that the environment has # been setup correctly. if [ ${PYTHON_ENV_VERSION} = "11" ]; then create_conda_env "3.11" "python-3-11" - conda install tensorflow=2.12.0 -y + conda install torch=2.6.0 -y conda install -c conda-forge libstdcxx-ng=14 -y conda install numpy=1.23.5 -y - EXPECTED_VERSION_STRING="Python version is 3.11, NumPy version is 1.23.5, and Tensorflow version is 2.12.0" + EXPECTED_VERSION_STRING="Python version is 3.11, NumPy version is 1.23.5, and PyTorch version is 2.6.0" create_python_backend_stub conda-pack -o python3.11.tar.gz path_to_conda_pack="$PWD/python-3-11" diff --git a/qa/L0_batch_custom/batch_custom_test.py b/qa/L0_batch_custom/batch_custom_test.py index 6cd6346ad3..1bd5c0d7f2 100755 --- a/qa/L0_batch_custom/batch_custom_test.py +++ b/qa/L0_batch_custom/batch_custom_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -88,9 +88,7 @@ def check_response( start_ms = int(round(time.time() * 1000)) if ( - trial == "savedmodel" - or trial == "graphdef" - or trial == "libtorch" + trial == "libtorch" or trial == "onnx" or trial == "plan" or trial == "python" diff --git a/qa/L0_batch_input/test.sh b/qa/L0_batch_input/test.sh index e780516ec4..6c2b0c0d69 100755 --- a/qa/L0_batch_input/test.sh +++ b/qa/L0_batch_input/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2020-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -54,7 +54,7 @@ SERVER_LOG="./inference_server.log" source ../common/util.sh # If BACKENDS not specified, set to all -BACKENDS=${BACKENDS:="onnx savedmodel plan libtorch"} +BACKENDS=${BACKENDS:="onnx plan libtorch"} rm -f $SERVER_LOG $CLIENT_LOG diff --git a/qa/L0_batcher/batcher_test.py b/qa/L0_batcher/batcher_test.py index 38e208c21e..780eb1e68f 100755 --- a/qa/L0_batcher/batcher_test.py +++ b/qa/L0_batcher/batcher_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2018-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -61,7 +61,7 @@ USE_GRPC = False assert USE_GRPC or USE_HTTP, "USE_GRPC or USE_HTTP must be non-zero" -BACKENDS = os.environ.get("BACKENDS", "graphdef savedmodel onnx libtorch plan python") +BACKENDS = os.environ.get("BACKENDS", "onnx libtorch plan python") _trials = BACKENDS.split(" ") @@ -153,9 +153,7 @@ def check_response( start_ms = int(round(time.time() * 1000)) if ( - trial == "savedmodel" - or trial == "graphdef" - or trial == "libtorch" + trial == "libtorch" or trial == "onnx" or trial == "plan" or trial == "python" diff --git a/qa/L0_batcher/test.sh b/qa/L0_batcher/test.sh index 2eed5e3f13..136fbe586d 100755 --- a/qa/L0_batcher/test.sh +++ b/qa/L0_batcher/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2018-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -74,8 +74,6 @@ if [ "$TEST_VALGRIND" -eq 1 ]; then test_multi_batch_different_shape_allow_ragged" fi -TF_VERSION=${TF_VERSION:=2} - # On windows the paths invoked by the script (running in WSL) must use # /mnt/c when needed but the paths on the tritonserver command-line # must be C:/ style. @@ -105,13 +103,13 @@ else fi fi -SERVER_ARGS_EXTRA="--backend-directory=${BACKEND_DIR} --backend-config=tensorflow,version=${TF_VERSION}" +SERVER_ARGS_EXTRA="--backend-directory=${BACKEND_DIR}" source ../common/util.sh RET=0 # If BACKENDS not specified, set to all -BACKENDS=${BACKENDS:="graphdef savedmodel onnx libtorch plan python"} +BACKENDS=${BACKENDS:="onnx libtorch plan python"} export BACKENDS # Basic batcher tests diff --git a/qa/L0_client_nobatch/client_test.py b/qa/L0_client_nobatch/client_test.py index 3288fc2ebf..f2e0fc398b 100755 --- a/qa/L0_client_nobatch/client_test.py +++ b/qa/L0_client_nobatch/client_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2018-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -43,12 +43,12 @@ class ClientNoBatchTest(tu.TestResultCollector): def test_nobatch_request_for_batching_model(self): input_size = 16 - # graphdef_int32_int8_int8 has a batching version with max batch size of 8. + # onnx_int32_int8_int8 has a batching version with max batch size of 8. # The server should return an error if the batch size is not included in the # input shapes. tensor_shape = (input_size,) for protocol in ["http", "grpc"]: - model_name = tu.get_model_name("graphdef", np.int32, np.int8, np.int8) + model_name = tu.get_model_name("onnx", np.int32, np.int8, np.int8) in0 = np.random.randint(low=0, high=100, size=tensor_shape, dtype=np.int32) in1 = np.random.randint(low=0, high=100, size=tensor_shape, dtype=np.int32) @@ -94,14 +94,12 @@ def test_nobatch_request_for_batching_model(self): def test_batch_request_for_nobatching_model(self): input_size = 16 - # graphdef_nobatch_int32_int8_int8 is non batching version. + # onnx_nobatch_int32_int8_int8 is non batching version. # The server should return an error if the batch size dimension # is included in the shape tensor_shape = (1, input_size) for protocol in ["http", "grpc"]: - model_name = tu.get_model_name( - "graphdef_nobatch", np.int32, np.int8, np.int8 - ) + model_name = tu.get_model_name("onnx_nobatch", np.int32, np.int8, np.int8) in0 = np.random.randint(low=0, high=100, size=tensor_shape, dtype=np.int32) in1 = np.random.randint(low=0, high=100, size=tensor_shape, dtype=np.int32) @@ -148,14 +146,12 @@ def test_batch_request_for_nobatching_model(self): def test_nobatch_request_for_nonbatching_model(self): input_size = 16 - # graphdef_int32_int8_int8 has a batching version with max batch size of 8. + # onnx_int32_int8_int8 has a batching version with max batch size of 8. # The server should return an error if the batch size is not included in the # input shapes. tensor_shape = (input_size,) for protocol in ["http", "grpc"]: - model_name = tu.get_model_name( - "graphdef_nobatch", np.int32, np.int8, np.int8 - ) + model_name = tu.get_model_name("onnx_nobatch", np.int32, np.int8, np.int8) in0 = np.random.randint(low=0, high=100, size=tensor_shape, dtype=np.int32) in1 = np.random.randint(low=0, high=100, size=tensor_shape, dtype=np.int32) @@ -195,12 +191,12 @@ def test_nobatch_request_for_nonbatching_model(self): def test_batch_request_for_batching_model(self): input_size = 16 - # graphdef_nobatch_int32_int8_int8 is non batching version. + # onnx_nobatch_int32_int8_int8 is non batching version. # The server should return an error if the batch size dimension # is included in the shape tensor_shape = (1, input_size) for protocol in ["http", "grpc"]: - model_name = tu.get_model_name("graphdef", np.int32, np.int8, np.int8) + model_name = tu.get_model_name("onnx", np.int32, np.int8, np.int8) in0 = np.random.randint(low=0, high=100, size=tensor_shape, dtype=np.int32) in1 = np.random.randint(low=0, high=100, size=tensor_shape, dtype=np.int32) diff --git a/qa/L0_client_nobatch/test.sh b/qa/L0_client_nobatch/test.sh index e768f385e5..cb1a05d660 100755 --- a/qa/L0_client_nobatch/test.sh +++ b/qa/L0_client_nobatch/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2018-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -47,7 +47,7 @@ EXPECTED_NUM_TESTS="4" DATADIR=/data/inferenceserver/${REPO_VERSION} MODELDIR="${PWD}/qa_model_repository" -rm -rf ${MODELDIR} && cp -r "${DATADIR}/qa_model_repository" ${MODELDIR} +rm -rf ${MODELDIR} && cp -r "${DATADIR}/qa_model_repository/onnx_*" ${MODELDIR} # Note there is a coupling in ./client_test.py SERVER=/opt/tritonserver/bin/tritonserver SERVER_ARGS="--model-repository=${MODELDIR}" SERVER_LOG="./inference_server.log" diff --git a/qa/L0_compute_capability/test.sh b/qa/L0_compute_capability/test.sh index d85acb1b6e..066b8fbf23 100755 --- a/qa/L0_compute_capability/test.sh +++ b/qa/L0_compute_capability/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2020-2021, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -50,7 +50,7 @@ rm -f *.log RET=0 # If BACKENDS not specified, set to all -BACKENDS=${BACKENDS:="graphdef savedmodel onnx libtorch plan"} +BACKENDS=${BACKENDS:="onnx libtorch plan"} for BACKEND in $BACKENDS; do # Need just one model for the backend... @@ -67,8 +67,8 @@ for BACKEND in $BACKENDS; do # Run with a high minimum capability so that no GPUs are # recognized. This should cause the server to fail to start since # we explicitly asked for a GPU in the instance_group. - SERVER_ARGS="--min-supported-compute-capability=100.0 --model-repository=`pwd`/models" - SERVER_LOG="./inference_server_${BACKEND}_cc100.log" + SERVER_ARGS="--min-supported-compute-capability=900.0 --model-repository=`pwd`/models" + SERVER_LOG="./inference_server_${BACKEND}_cc900.log" run_server if [ "$SERVER_PID" != "0" ]; then echo -e "\n***\n*** Unexpected success with min compute 100.0 for ${BACKEND}\n***" diff --git a/qa/L0_config_json/ensemble_config.pbtxt b/qa/L0_config_json/ensemble_config.pbtxt index 29de01a3aa..b1fd8972b5 100644 --- a/qa/L0_config_json/ensemble_config.pbtxt +++ b/qa/L0_config_json/ensemble_config.pbtxt @@ -1,4 +1,4 @@ -# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2020-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -54,7 +54,7 @@ output [ ensemble_scheduling { step [ { - model_name: "savedmodel_nobatch_float32_float32_float32" + model_name: "onnx_nobatch_float32_float32_float32" model_version: 1 input_map [ { @@ -78,7 +78,7 @@ ensemble_scheduling { ] }, { - model_name: "savedmodel_nobatch_float32_float32_float32" + model_name: "onnx_nobatch_float32_float32_float32" model_version: -1 input_map [ { diff --git a/qa/L0_config_json/test.sh b/qa/L0_config_json/test.sh index b1016b806b..7d073d7bb2 100755 --- a/qa/L0_config_json/test.sh +++ b/qa/L0_config_json/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2020-2023, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2020-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -52,7 +52,7 @@ RET=0 rm -fr *.log rm -fr models && mkdir models -cp -r $DATADIR/qa_model_repository/savedmodel_nobatch_float32_float32_float32 models/. +cp -r $DATADIR/qa_model_repository/onnx_nobatch_float32_float32_float32 models/. # Test input and output dims are shown as numbers TRIAL=ios @@ -65,7 +65,7 @@ if [ "$SERVER_PID" == "0" ]; then fi set +e -code=`curl -s -w %{http_code} -o ./$TRIAL.out localhost:8000/v2/models/savedmodel_nobatch_float32_float32_float32/config` +code=`curl -s -w %{http_code} -o ./$TRIAL.out localhost:8000/v2/models/onnx_nobatch_float32_float32_float32/config` set -e if [ "$code" != "200" ]; then cat $TRIAL.out @@ -87,8 +87,8 @@ wait $SERVER_PID TRIAL=reshape rm -fr models && mkdir models -cp -r $DATADIR/qa_model_repository/savedmodel_nobatch_float32_float32_float32 models/. -(cd models/savedmodel_nobatch_float32_float32_float32 && \ +cp -r $DATADIR/qa_model_repository/onnx_nobatch_float32_float32_float32 models/. +(cd models/onnx_nobatch_float32_float32_float32 && \ sed -i "s/data_type:.*TYPE_FP32/data_type: TYPE_FP32\nreshape: { shape: [ 16 ]}/g" config.pbtxt) run_server @@ -99,7 +99,7 @@ if [ "$SERVER_PID" == "0" ]; then fi set +e -code=`curl -s -w %{http_code} -o ./$TRIAL.out localhost:8000/v2/models/savedmodel_nobatch_float32_float32_float32/config` +code=`curl -s -w %{http_code} -o ./$TRIAL.out localhost:8000/v2/models/onnx_nobatch_float32_float32_float32/config` set -e if [ "$code" != "200" ]; then cat $TRIAL.out @@ -121,8 +121,8 @@ wait $SERVER_PID TRIAL=specific rm -fr models && mkdir models -cp -r $DATADIR/qa_model_repository/savedmodel_nobatch_float32_float32_float32 models/. -(cd models/savedmodel_nobatch_float32_float32_float32 && \ +cp -r $DATADIR/qa_model_repository/onnx_nobatch_float32_float32_float32 models/. +(cd models/onnx_nobatch_float32_float32_float32 && \ sed -i "s/^version_policy:.*/version_policy: { specific: { versions: [1] }}/" config.pbtxt) run_server @@ -133,7 +133,7 @@ if [ "$SERVER_PID" == "0" ]; then fi set +e -code=`curl -s -w %{http_code} -o ./$TRIAL.out localhost:8000/v2/models/savedmodel_nobatch_float32_float32_float32/config` +code=`curl -s -w %{http_code} -o ./$TRIAL.out localhost:8000/v2/models/onnx_nobatch_float32_float32_float32/config` set -e if [ "$code" != "200" ]; then cat $TRIAL.out @@ -157,8 +157,8 @@ wait $SERVER_PID TRIAL=dbatch rm -fr models && mkdir models -cp -r $DATADIR/qa_model_repository/savedmodel_nobatch_float32_float32_float32 models/. -(cd models/savedmodel_nobatch_float32_float32_float32 && \ +cp -r $DATADIR/qa_model_repository/onnx_nobatch_float32_float32_float32 models/. +(cd models/onnx_nobatch_float32_float32_float32 && \ echo "dynamic_batching: { max_queue_delay_microseconds: 42 \ default_queue_policy: { default_timeout_microseconds: 123 } \ priority_queue_policy: { key: 1 value: { default_timeout_microseconds: 123 }} \ @@ -172,7 +172,7 @@ if [ "$SERVER_PID" == "0" ]; then fi set +e -code=`curl -s -w %{http_code} -o ./$TRIAL.out localhost:8000/v2/models/savedmodel_nobatch_float32_float32_float32/config` +code=`curl -s -w %{http_code} -o ./$TRIAL.out localhost:8000/v2/models/onnx_nobatch_float32_float32_float32/config` set -e if [ "$code" != "200" ]; then cat $TRIAL.out @@ -209,8 +209,8 @@ wait $SERVER_PID TRIAL=sbatch rm -fr models && mkdir models -cp -r $DATADIR/qa_model_repository/savedmodel_nobatch_float32_float32_float32 models/. -(cd models/savedmodel_nobatch_float32_float32_float32 && \ +cp -r $DATADIR/qa_model_repository/onnx_nobatch_float32_float32_float32 models/. +(cd models/onnx_nobatch_float32_float32_float32 && \ echo "sequence_batching: { max_sequence_idle_microseconds: 42 \ oldest: { max_queue_delay_microseconds: 987 }}" >> config.pbtxt) @@ -222,7 +222,7 @@ if [ "$SERVER_PID" == "0" ]; then fi set +e -code=`curl -s -w %{http_code} -o ./$TRIAL.out localhost:8000/v2/models/savedmodel_nobatch_float32_float32_float32/config` +code=`curl -s -w %{http_code} -o ./$TRIAL.out localhost:8000/v2/models/onnx_nobatch_float32_float32_float32/config` set -e if [ "$code" != "200" ]; then cat $TRIAL.out @@ -265,7 +265,7 @@ wait $SERVER_PID TRIAL=ensemble rm -fr models && mkdir models -cp -r $DATADIR/qa_model_repository/savedmodel_nobatch_float32_float32_float32 models/. +cp -r $DATADIR/qa_model_repository/onnx_nobatch_float32_float32_float32 models/. mkdir -p models/simple_ensemble/1 && cp ensemble_config.pbtxt models/simple_ensemble/config.pbtxt run_server @@ -307,8 +307,8 @@ rm -fr models/simple_ensemble TRIAL=warmup rm -fr models && mkdir models -cp -r $DATADIR/qa_model_repository/savedmodel_nobatch_float32_float32_float32 models/. -(cd models/savedmodel_nobatch_float32_float32_float32 && \ +cp -r $DATADIR/qa_model_repository/onnx_nobatch_float32_float32_float32 models/. +(cd models/onnx_nobatch_float32_float32_float32 && \ echo "model_warmup [{" >> config.pbtxt && \ echo " name : \"warmup 1\"" >> config.pbtxt && \ echo " batch_size: 1" >> config.pbtxt && \ @@ -355,7 +355,7 @@ if [ "$SERVER_PID" == "0" ]; then fi set +e -code=`curl -s -w %{http_code} -o ./$TRIAL.out localhost:8000/v2/models/savedmodel_nobatch_float32_float32_float32/config` +code=`curl -s -w %{http_code} -o ./$TRIAL.out localhost:8000/v2/models/onnx_nobatch_float32_float32_float32/config` set -e if [ "$code" != "200" ]; then cat $TRIAL.out diff --git a/qa/L0_custom_model_config/test.sh b/qa/L0_custom_model_config/test.sh index d839cacbd5..99e9ed8b10 100755 --- a/qa/L0_custom_model_config/test.sh +++ b/qa/L0_custom_model_config/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -51,8 +51,8 @@ RET=0 rm -fr *.log rm -fr models && mkdir models -cp -r $DATADIR/qa_model_repository/savedmodel_nobatch_float32_float32_float32 models/. -mkdir models/savedmodel_nobatch_float32_float32_float32/configs +cp -r $DATADIR/qa_model_repository/onnx_nobatch_float32_float32_float32 models/. +mkdir models/onnx_nobatch_float32_float32_float32/configs test_custom_config() { @@ -66,7 +66,7 @@ test_custom_config() fi set +e - code=`curl -s -w %{http_code} -o ./curl.out localhost:8000/v2/models/savedmodel_nobatch_float32_float32_float32/config` + code=`curl -s -w %{http_code} -o ./curl.out localhost:8000/v2/models/onnx_nobatch_float32_float32_float32/config` set -e if [ "$code" != "200" ]; then cat $out.out @@ -92,15 +92,15 @@ VERSION_V100="2" VERSION_CUSTOM="3" # Distinguish configs with different model versions -(cd models/savedmodel_nobatch_float32_float32_float32 && \ +(cd models/onnx_nobatch_float32_float32_float32 && \ sed -i "s/^version_policy:.*/version_policy: { specific: { versions: [$VERSION_DEFAULT] }}/" config.pbtxt) -(cd models/savedmodel_nobatch_float32_float32_float32 && \ +(cd models/onnx_nobatch_float32_float32_float32 && \ cp config.pbtxt configs/h100.pbtxt && \ sed -i "s/^version_policy:.*/version_policy: { specific: { versions: [$VERSION_H100] }}/" configs/h100.pbtxt) -(cd models/savedmodel_nobatch_float32_float32_float32 && \ +(cd models/onnx_nobatch_float32_float32_float32 && \ cp config.pbtxt configs/v100.pbtxt && \ sed -i "s/^version_policy:.*/version_policy: { specific: { versions: [$VERSION_V100] }}/" configs/v100.pbtxt) -(cd models/savedmodel_nobatch_float32_float32_float32 && \ +(cd models/onnx_nobatch_float32_float32_float32 && \ cp config.pbtxt configs/config.pbtxt && \ sed -i "s/^version_policy:.*/version_policy: { specific: { versions: [$VERSION_CUSTOM] }}/" configs/config.pbtxt) diff --git a/qa/L0_custom_ops/cuda_op_test.py b/qa/L0_custom_ops/cuda_op_test.py deleted file mode 100755 index bc610b3f0b..0000000000 --- a/qa/L0_custom_ops/cuda_op_test.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/python - -# Copyright (c) 2019-2020, NVIDIA CORPORATION. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import argparse -import sys -from builtins import range - -import numpy as np -import tritonclient.grpc as grpcclient -import tritonclient.http as httpclient -from tritonclient.utils import np_to_triton_dtype - -FLAGS = None - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument( - "-v", - "--verbose", - action="store_true", - required=False, - default=False, - help="Enable verbose output", - ) - parser.add_argument( - "-u", - "--url", - type=str, - required=False, - default="localhost:8000", - help="Inference server URL. Default is localhost:8000.", - ) - parser.add_argument( - "-i", - "--protocol", - type=str, - required=False, - default="http", - help='Protocol ("http"/"grpc") used to ' - + 'communicate with inference service. Default is "http".', - ) - parser.add_argument("-m", "--model", type=str, required=True, help="Name of model.") - - FLAGS = parser.parse_args() - if (FLAGS.protocol != "http") and (FLAGS.protocol != "grpc"): - print( - 'unexpected protocol "{}", expects "http" or "grpc"'.format(FLAGS.protocol) - ) - exit(1) - - client_util = httpclient if FLAGS.protocol == "http" else grpcclient - - # Run the cudaop model, which depends on a custom operation that - # uses CUDA. The custom operator adds one to each input - model_name = FLAGS.model - elements = 8 - - # Create the inference context for the model. - client = client_util.InferenceServerClient(FLAGS.url, verbose=FLAGS.verbose) - - # Create the data for one input tensor. - input_data = np.arange(start=42, stop=42 + elements, dtype=np.int32) - - inputs = [ - client_util.InferInput( - "in", input_data.shape, np_to_triton_dtype(input_data.dtype) - ) - ] - inputs[0].set_data_from_numpy(input_data) - - results = client.infer(model_name, inputs) - output_data = results.as_numpy("out") - if output_data is None: - print("error: expected 'out'") - sys.exit(1) - - for i in range(elements): - print( - str(i) + ": input " + str(input_data[i]) + ", output " + str(output_data[i]) - ) - if output_data[i] != (input_data[i] + 1): - print("error: incorrect value") - sys.exit(1) diff --git a/qa/L0_custom_ops/test.sh b/qa/L0_custom_ops/test.sh index f3b792e59a..60cb5d7a82 100755 --- a/qa/L0_custom_ops/test.sh +++ b/qa/L0_custom_ops/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -41,8 +41,6 @@ fi export CUDA_VISIBLE_DEVICES=0 CLIENT_LOG="./client.log" -ZERO_OUT_TEST=zero_out_test.py -CUDA_OP_TEST=cuda_op_test.py MOD_OP_TEST=mod_op_test.py VISION_OP_TEST=vision_op_test.py ONNX_OP_TEST=onnx_op_test.py @@ -55,113 +53,6 @@ rm -f $SERVER_LOG $CLIENT_LOG RET=0 -# Must explicitly set LD_LIBRARY_PATH so that the custom operations -# can find libtensorflow_framework.so. -LD_LIBRARY_PATH=/opt/tritonserver/backends/tensorflow:$LD_LIBRARY_PATH - -# Tensorflow -## Load operations via LD_PRELOAD -SERVER_ARGS="--model-repository=/data/inferenceserver/${REPO_VERSION}/qa_custom_ops/tf_custom_ops" -SERVER_LD_PRELOAD="/data/inferenceserver/${REPO_VERSION}/qa_custom_ops/tf_custom_ops/libzeroout.so:/data/inferenceserver/${REPO_VERSION}/qa_custom_ops/tf_custom_ops/libcudaop.so:/data/inferenceserver/${REPO_VERSION}/qa_custom_ops/tf_custom_ops/libbusyop.so" - -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 -fi - -set +e - -python $ZERO_OUT_TEST -v -m graphdef_zeroout >>$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -python $ZERO_OUT_TEST -v -m savedmodel_zeroout >>$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -python $CUDA_OP_TEST -v -m graphdef_cudaop >>$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -python $CUDA_OP_TEST -v -m savedmodel_cudaop >>$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -set -e - -kill $SERVER_PID -wait $SERVER_PID - -## Load operations via model config -SERVER_ARGS="--model-repository=tf_custom_ops" -SERVER_LD_PRELOAD="" - -rm -rf tf_custom_ops && \ - mkdir -p tf_custom_ops && \ - cp -r /data/inferenceserver/${REPO_VERSION}/qa_custom_ops/tf_custom_ops . - -for MODEL_TYPE in savedmodel graphdef; do - echo "model_operations { op_library_filename: \"tf_custom_ops/libbusyop.so\" }" >> tf_custom_ops/${MODEL_TYPE}_busyop/config.pbtxt - echo "model_operations { op_library_filename: \"tf_custom_ops/libcudaop.so\" }" >> tf_custom_ops/${MODEL_TYPE}_cudaop/config.pbtxt - echo "model_operations { op_library_filename: \"tf_custom_ops/libzeroout.so\" }" >> tf_custom_ops/${MODEL_TYPE}_zeroout/config.pbtxt -done - -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 -fi - -set +e - -python $ZERO_OUT_TEST -v -m graphdef_zeroout >>$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -python $ZERO_OUT_TEST -v -m savedmodel_zeroout >>$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -python $CUDA_OP_TEST -v -m graphdef_cudaop >>$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -python $CUDA_OP_TEST -v -m savedmodel_cudaop >>$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -set -e - -kill $SERVER_PID -wait $SERVER_PID - # Must set LD_LIBRARY_PATH just for the server launch so that the # custom operations can find libtorch.so and other pytorch dependencies. LD_LIBRARY_PATH=/opt/tritonserver/backends/pytorch:$LD_LIBRARY_PATH diff --git a/qa/L0_custom_ops/zero_out_test.py b/qa/L0_custom_ops/zero_out_test.py deleted file mode 100755 index 86fdcb8a30..0000000000 --- a/qa/L0_custom_ops/zero_out_test.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/python - -# Copyright (c) 2019-2020, NVIDIA CORPORATION. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import argparse -import sys -from builtins import range - -import numpy as np -import tritonclient.grpc as grpcclient -import tritonclient.http as httpclient -from tritonclient.utils import np_to_triton_dtype - -FLAGS = None - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument( - "-v", - "--verbose", - action="store_true", - required=False, - default=False, - help="Enable verbose output", - ) - parser.add_argument( - "-u", - "--url", - type=str, - required=False, - default="localhost:8000", - help="Inference server URL. Default is localhost:8000.", - ) - parser.add_argument( - "-i", - "--protocol", - type=str, - required=False, - default="http", - help='Protocol ("http"/"grpc") used to ' - + 'communicate with inference service. Default is "http".', - ) - parser.add_argument("-m", "--model", type=str, required=True, help="Name of model.") - - FLAGS = parser.parse_args() - if (FLAGS.protocol != "http") and (FLAGS.protocol != "grpc"): - print( - 'unexpected protocol "{}", expects "http" or "grpc"'.format(FLAGS.protocol) - ) - exit(1) - - client_util = httpclient if FLAGS.protocol == "http" else grpcclient - - # Run the zero-out model, which depends on a custom operation - model_name = FLAGS.model - elements = 8 - - # Create the inference context for the model. - client = client_util.InferenceServerClient(FLAGS.url, verbose=FLAGS.verbose) - - # Create the data for one input tensor. - input_data = np.arange(start=42, stop=42 + elements, dtype=np.int32) - - inputs = [ - client_util.InferInput( - "to_zero", input_data.shape, np_to_triton_dtype(input_data.dtype) - ) - ] - inputs[0].set_data_from_numpy(input_data) - results = client.infer(model_name, inputs) - - # We expect 1 result with all inputs except first to be zeroed. - output_data = results.as_numpy("zeroed") - if output_data is None: - print("error: expected 'zeroed'") - sys.exit(1) - - for i in range(elements): - print( - str(i) + ": input " + str(input_data[i]) + ", output " + str(output_data[i]) - ) - if (i == 0) and (input_data[i] != output_data[i]): - print("error: incorrect value") - sys.exit(1) - if (i != 0) and (output_data[i] != 0): - print("error: expected 0") - sys.exit(1) diff --git a/qa/L0_dlpack_multi_gpu/test.sh b/qa/L0_dlpack_multi_gpu/test.sh index a90169780c..d85a089431 100755 --- a/qa/L0_dlpack_multi_gpu/test.sh +++ b/qa/L0_dlpack_multi_gpu/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -42,7 +42,6 @@ source ../common/util.sh # Uninstall the non CUDA version of PyTorch pip3 uninstall -y torch pip3 install torch==2.3.1+cu118 -f https://download.pytorch.org/whl/torch_stable.html -pip3 install tensorflow # Install CuPy for testing non_blocking compute streams pip3 install cupy-cuda12x diff --git a/qa/L0_dyna_sequence_batcher/dyna_sequence_batcher_test.py b/qa/L0_dyna_sequence_batcher/dyna_sequence_batcher_test.py index f2c709469b..47a0e2da32 100755 --- a/qa/L0_dyna_sequence_batcher/dyna_sequence_batcher_test.py +++ b/qa/L0_dyna_sequence_batcher/dyna_sequence_batcher_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2019-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -44,9 +44,7 @@ _test_cuda_shared_memory = bool(int(os.environ.get("TEST_CUDA_SHARED_MEMORY", 0))) NO_BATCHING = int(os.environ.get("NO_BATCHING", 0)) == 1 -BACKENDS = os.environ.get( - "BACKENDS", "graphdef savedmodel libtorch onnx plan custom custom_string" -) +BACKENDS = os.environ.get("BACKENDS", "libtorch onnx plan custom custom_string") IMPLICIT_STATE = int(os.environ["IMPLICIT_STATE"]) == 1 _trials = BACKENDS.split(" ") @@ -74,7 +72,6 @@ def get_expected_result(self, expected_result, corrid, value, trial, flag_str=No # information. if ( (("nobatch" not in trial) and ("custom" not in trial)) - or ("graphdef" in trial) or ("plan" in trial) or ("onnx" in trial) or ("libtorch" in trial) diff --git a/qa/L0_dyna_sequence_batcher/test.sh b/qa/L0_dyna_sequence_batcher/test.sh index d545886293..afa2bb35f9 100755 --- a/qa/L0_dyna_sequence_batcher/test.sh +++ b/qa/L0_dyna_sequence_batcher/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -53,7 +53,7 @@ IMPLICIT_STATE=${IMPLICIT_STATE:="0"} export IMPLICIT_STATE # If BACKENDS not specified, set to all -BACKENDS=${BACKENDS:="graphdef savedmodel libtorch onnx plan custom custom_string"} +BACKENDS=${BACKENDS:="libtorch onnx plan custom custom_string"} export BACKENDS MODEL_REPOSITORY='' diff --git a/qa/L0_grpc/test.sh b/qa/L0_grpc/test.sh index 93d22e75be..28d9236d0a 100755 --- a/qa/L0_grpc/test.sh +++ b/qa/L0_grpc/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -149,7 +149,7 @@ cp -r ${MODELDIR}/simple_dyna_sequence ${MODELDIR}/simple_string_dyna_sequence sed -i "s/simple_dyna_sequence/simple_string_dyna_sequence/g" ${MODELDIR}/simple_string_dyna_sequence/config.pbtxt sed -i "s/^platform: .*/backend: \"dyna_sequence\"/g" ${MODELDIR}/simple_string_dyna_sequence/config.pbtxt sed -i "/CONTROL_SEQUENCE_CORRID/{n;s/data_type:.*/data_type: TYPE_STRING/}" ${MODELDIR}/simple_string_dyna_sequence/config.pbtxt -rm -f ${MODELDIR}/simple_string_dyna_sequence/1/model.graphdef +rm -f ${MODELDIR}/simple_string_dyna_sequence/1/model.onnx cp ../custom_models/custom_dyna_sequence_int32/1/libtriton_dyna_sequence.so ${MODELDIR}/simple_string_dyna_sequence/1/ rm -f *.log @@ -205,19 +205,19 @@ for i in \ EXTRA_ARGS="-i grpc -u localhost:8001" fi if [[ ($SUFFIX == "image_client") || ($SUFFIX == "grpc_image_client") ]]; then - python $i -m inception_graphdef -s INCEPTION -a -c 1 -b 1 $EXTRA_ARGS $IMAGE >> "${CLIENT_LOG}.async.${SUFFIX}" 2>&1 + python $i -m inception_onnx -s INCEPTION -a -c 1 -b 1 $EXTRA_ARGS $IMAGE >> "${CLIENT_LOG}.async.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.async.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.async.${SUFFIX} RET=1 fi - python $i -m inception_graphdef -s INCEPTION -a --streaming -c 1 -b 1 $EXTRA_ARGS $IMAGE >> "${CLIENT_LOG}.streaming.${SUFFIX}" 2>&1 + python $i -m inception_onnx -s INCEPTION -a --streaming -c 1 -b 1 $EXTRA_ARGS $IMAGE >> "${CLIENT_LOG}.streaming.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.streaming.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.streaming.${SUFFIX} RET=1 fi - python $i -m inception_graphdef -s INCEPTION -c 1 -b 1 $EXTRA_ARGS $IMAGE >> "${CLIENT_LOG}.${SUFFIX}" 2>&1 + python $i -m inception_onnx -s INCEPTION -c 1 -b 1 $EXTRA_ARGS $IMAGE >> "${CLIENT_LOG}.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.${SUFFIX} @@ -270,19 +270,19 @@ for i in \ BASE=$(basename -- $i) SUFFIX="${BASE%.*}" if [[ $SUFFIX == "image_client" ]]; then - $i -m inception_graphdef -s INCEPTION -a -c 1 -b 1 -i grpc -u localhost:8001 $IMAGE >> "${CLIENT_LOG}.c++.async.${SUFFIX}" 2>&1 + $i -m inception_onnx -s INCEPTION -a -c 1 -b 1 -i grpc -u localhost:8001 $IMAGE >> "${CLIENT_LOG}.c++.async.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.c++.async.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.c++.${SUFFIX} RET=1 fi - $i -m inception_graphdef -s INCEPTION -a --streaming -c 1 -b 1 -i grpc -u localhost:8001 $IMAGE >> "${CLIENT_LOG}.c++.streaming.${SUFFIX}" 2>&1 + $i -m inception_onnx -s INCEPTION -a --streaming -c 1 -b 1 -i grpc -u localhost:8001 $IMAGE >> "${CLIENT_LOG}.c++.streaming.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.c++.streaming.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.c++.${SUFFIX} RET=1 fi - $i -m inception_graphdef -s INCEPTION -c 1 -b 1 -i grpc -u localhost:8001 $IMAGE >> "${CLIENT_LOG}.c++.${SUFFIX}" 2>&1 + $i -m inception_onnx -s INCEPTION -c 1 -b 1 -i grpc -u localhost:8001 $IMAGE >> "${CLIENT_LOG}.c++.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.c++.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.c++.${SUFFIX} diff --git a/qa/L0_http/test.sh b/qa/L0_http/test.sh index c90c2e5010..c36024e007 100755 --- a/qa/L0_http/test.sh +++ b/qa/L0_http/test.sh @@ -119,7 +119,7 @@ cp -r ${MODELDIR}/simple_dyna_sequence ${MODELDIR}/simple_string_dyna_sequence sed -i "s/simple_dyna_sequence/simple_string_dyna_sequence/g" ${MODELDIR}/simple_string_dyna_sequence/config.pbtxt sed -i "s/^platform: .*/backend: \"dyna_sequence\"/g" ${MODELDIR}/simple_string_dyna_sequence/config.pbtxt sed -i "/CONTROL_SEQUENCE_CORRID/{n;s/data_type:.*/data_type: TYPE_STRING/}" ${MODELDIR}/simple_string_dyna_sequence/config.pbtxt -rm -f ${MODELDIR}/simple_string_dyna_sequence/1/model.graphdef +rm -f ${MODELDIR}/simple_string_dyna_sequence/1/model.onnx cp ../custom_models/custom_dyna_sequence_int32/1/libtriton_dyna_sequence.so ${MODELDIR}/simple_string_dyna_sequence/1/ rm -f *.log @@ -163,13 +163,13 @@ for i in \ BASE=$(basename -- $i) SUFFIX="${BASE%.*}" if [ $SUFFIX == "image_client" ]; then - python $i -m inception_graphdef -s INCEPTION -a -c 1 -b 1 $IMAGE >> "${CLIENT_LOG}.async.${SUFFIX}" 2>&1 + python $i -m inception_onnx -s INCEPTION -a -c 1 -b 1 $IMAGE >> "${CLIENT_LOG}.async.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.async.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.async.${SUFFIX} RET=1 fi - python $i -m inception_graphdef -s INCEPTION -c 1 -b 1 $IMAGE >> "${CLIENT_LOG}.${SUFFIX}" 2>&1 + python $i -m inception_onnx -s INCEPTION -c 1 -b 1 $IMAGE >> "${CLIENT_LOG}.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.${SUFFIX} diff --git a/qa/L0_http_fuzz/test.sh b/qa/L0_http_fuzz/test.sh index 30cf02bc2a..1abe08b487 100755 --- a/qa/L0_http_fuzz/test.sh +++ b/qa/L0_http_fuzz/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2020-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -46,7 +46,7 @@ rm -f *.log *.db EXPECTED_NUM_TESTS="1" mkdir -p models -cp -r /data/inferenceserver/${REPO_VERSION}/qa_identity_model_repository/savedmodel_zero_1_object models/ +cp -r /data/inferenceserver/${REPO_VERSION}/qa_identity_model_repository/onnx_zero_1_object models/ FUZZTEST=fuzztest.py FUZZ_LOG=`pwd`/fuzz.log diff --git a/qa/L0_infer/infer_test.py b/qa/L0_infer/infer_test.py index 63433f01c9..8f16a7fe10 100755 --- a/qa/L0_infer/infer_test.py +++ b/qa/L0_infer/infer_test.py @@ -48,7 +48,7 @@ assert USE_GRPC or USE_HTTP, "USE_GRPC or USE_HTTP must be non-zero" BACKENDS = os.environ.get( - "BACKENDS", "graphdef savedmodel onnx libtorch plan python python_dlpack openvino" + "BACKENDS", "onnx libtorch plan python python_dlpack openvino" ) ENSEMBLES = bool(int(os.environ.get("ENSEMBLES", 1))) NOBATCH = bool(int(os.environ.get("NOBATCH", 1))) @@ -163,7 +163,7 @@ def _infer_exact_helper( ): ensemble_prefix.append(prefix) - if tu.validate_for_tf_model( + if tu.validate_for_onnx_model( input_dtype, output0_dtype, output1_dtype, @@ -172,7 +172,7 @@ def _infer_exact_helper( (input_size,), ): for prefix in ensemble_prefix: - for pf in ["graphdef", "savedmodel"]: + for pf in ["onnx"]: if pf in BACKENDS: _infer_exact_helper( self, @@ -670,9 +670,9 @@ def test_raw_version_latest_1(self): input_size = 16 tensor_shape = (1, input_size) - # There are 3 versions of graphdef_int8_int8_int8 but + # There are 3 versions of onnx_int8_int8_int8 but # only version 3 should be available - for platform in ("graphdef", "savedmodel"): + for platform in ["onnx"]: if platform not in BACKENDS: continue try: @@ -733,9 +733,9 @@ def test_raw_version_latest_2(self): input_size = 16 tensor_shape = (1, input_size) - # There are 3 versions of graphdef_int16_int16_int16 but only + # There are 3 versions of onnx_int16_int16_int16 but only # versions 2 and 3 should be available - for platform in ("graphdef", "savedmodel"): + for platform in ["onnx"]: if platform not in BACKENDS: continue try: @@ -794,7 +794,7 @@ def test_raw_version_all(self): # There are 3 versions of *_int32_int32_int32 and all should # be available. - for platform in ("graphdef", "savedmodel"): + for platform in ["onnx"]: if platform not in BACKENDS: continue iu.infer_exact( @@ -849,7 +849,7 @@ def test_raw_version_specific_1(self): # There are 3 versions of *_float16_float16_float16 but only # version 1 should be available. - for platform in ("graphdef", "savedmodel"): + for platform in ["onnx"]: if platform not in BACKENDS: continue iu.infer_exact( @@ -911,7 +911,7 @@ def test_raw_version_specific_1_3(self): # There are 3 versions of *_float32_float32_float32 but only # versions 1 and 3 should be available. - for platform in ("graphdef", "savedmodel", "plan"): + for platform in ("onnx", "plan"): if platform == "plan" and CPU_ONLY: continue if platform not in BACKENDS: @@ -969,7 +969,7 @@ def test_raw_version_specific_1_3(self): ) if ENSEMBLES: - if all(x in BACKENDS for x in ["graphdef", "savedmodel"]): + if all(x in BACKENDS for x in ["onnx", "plan"]): def test_ensemble_mix_platform(self): # Skip on CPU only machine as TensorRT model is used in this ensemble @@ -990,7 +990,7 @@ def test_ensemble_mix_platform(self): use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) - if "graphdef" in BACKENDS: + if "onnx" in BACKENDS: def test_ensemble_mix_type(self): for bs in (1, 8): @@ -1008,7 +1008,7 @@ def test_ensemble_mix_type(self): use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) - if all(x in BACKENDS for x in ["graphdef", "savedmodel"]): + if all(x in BACKENDS for x in ["onnx", "plan"]): def test_ensemble_mix_ensemble(self): for bs in (1, 8): @@ -1029,7 +1029,7 @@ def test_ensemble_mix_ensemble(self): if all( x in BACKENDS for x in [ - "graphdef", + "onnx", ] ): @@ -1083,7 +1083,7 @@ def test_ensemble_mix_batch_nobatch(self): if not (TEST_SYSTEM_SHARED_MEMORY or TEST_CUDA_SHARED_MEMORY): def test_ensemble_label_lookup(self): - if all(x in BACKENDS for x in ["graphdef", "savedmodel"]): + if all(x in BACKENDS for x in ["onnx", "plan"]): # Ensemble needs to look up label from the actual model for bs in (1, 8): iu.infer_exact( @@ -1102,7 +1102,7 @@ def test_ensemble_label_lookup(self): use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) - if all(x in BACKENDS for x in ["graphdef", "savedmodel"]): + if all(x in BACKENDS for x in ["onnx", "plan"]): # Label from the actual model will be passed along the nested ensemble for bs in (1, 8): iu.infer_exact( @@ -1121,7 +1121,7 @@ def test_ensemble_label_lookup(self): use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) - if "graphdef" in BACKENDS: + if "onnx" in BACKENDS: # If label file is provided, it will use the provided label file directly try: iu.infer_exact( @@ -1144,7 +1144,7 @@ def test_ensemble_label_lookup(self): # with unexpected labels pass - if "graphdef" in BACKENDS: + if "onnx" in BACKENDS: for bs in (1, 8): iu.infer_exact( self, diff --git a/qa/L0_infer/test.sh b/qa/L0_infer/test.sh index 2d54fd8965..79ba093ffd 100755 --- a/qa/L0_infer/test.sh +++ b/qa/L0_infer/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2018-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -67,16 +67,15 @@ if [ "$TEST_VALGRIND" -eq 1 ]; then rm -f $LEAKCHECK_LOG_BASE* # Remove 'python', 'python_dlpack' and 'onnx' from BACKENDS and test them # separately below. - BACKENDS="graphdef savedmodel libtorch plan openvino" + BACKENDS="libtorch plan openvino" fi if [ "$TEST_SYSTEM_SHARED_MEMORY" -eq 1 ] || [ "$TEST_CUDA_SHARED_MEMORY" -eq 1 ]; then EXPECTED_NUM_TESTS=${EXPECTED_NUM_TESTS:="33"} else - EXPECTED_NUM_TESTS=${EXPECTED_NUM_TESTS:="46"} + EXPECTED_NUM_TESTS=${EXPECTED_NUM_TESTS:="44"} fi -TF_VERSION=${TF_VERSION:=2} TEST_JETSON=${TEST_JETSON:=0} # Default size (in MB) of shared memory to be used by each python model @@ -109,7 +108,7 @@ else fi # Allow more time to exit. Ensemble brings in too many models -SERVER_ARGS_EXTRA="--exit-timeout-secs=${SERVER_TIMEOUT} --backend-directory=${BACKEND_DIR} --backend-config=tensorflow,version=${TF_VERSION} --backend-config=python,stub-timeout-seconds=120 --backend-config=python,shm-default-byte-size=${DEFAULT_SHM_SIZE_BYTES}" +SERVER_ARGS_EXTRA="--exit-timeout-secs=${SERVER_TIMEOUT} --backend-directory=${BACKEND_DIR} --backend-config=python,stub-timeout-seconds=120 --backend-config=python,shm-default-byte-size=${DEFAULT_SHM_SIZE_BYTES}" SERVER_ARGS="--model-repository=${MODELDIR} ${SERVER_ARGS_EXTRA}" SERVER_LOG_BASE="./inference_server" source ../common/util.sh @@ -129,7 +128,7 @@ if [ "$TRITON_SERVER_CPU_ONLY" == "1" ]; then fi # If BACKENDS not specified, set to all -BACKENDS=${BACKENDS:="graphdef savedmodel onnx libtorch plan python python_dlpack openvino"} +BACKENDS=${BACKENDS:="onnx libtorch plan python python_dlpack openvino"} export BACKENDS # If ENSEMBLES not specified, set to 1 @@ -234,12 +233,12 @@ function generate_model_repository() { create_nop_version_dir `pwd`/models - if [[ $BACKENDS == *"graphdef"* ]]; then + if [[ $BACKENDS == *"onnx"* ]]; then ENSEMBLE_MODELS="wrong_label_int32_float32_float32 label_override_int32_float32_float32 mix_type_int32_float32_float32" ENSEMBLE_MODELS="${ENSEMBLE_MODELS} batch_to_nobatch_float32_float32_float32 batch_to_nobatch_nobatch_float32_float32_float32 nobatch_to_batch_float32_float32_float32 nobatch_to_batch_nobatch_float32_float32_float32 mix_nobatch_batch_float32_float32_float32" - if [[ $BACKENDS == *"savedmodel"* ]] ; then + if [[ $BACKENDS == *"libtorch"* ]] ; then ENSEMBLE_MODELS="${ENSEMBLE_MODELS} mix_platform_float32_float32_float32 mix_ensemble_int32_float32_float32" fi diff --git a/qa/L0_infer_reshape/infer_reshape_test.py b/qa/L0_infer_reshape/infer_reshape_test.py index e77dcbecaf..5445f5d6c9 100755 --- a/qa/L0_infer_reshape/infer_reshape_test.py +++ b/qa/L0_infer_reshape/infer_reshape_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2019-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -50,68 +50,6 @@ def _full_reshape(self, dtype, input_shapes, output_shapes=None, no_batch=True): output_shapes = input_shapes # For validation assume any shape can be used... - if tu.validate_for_tf_model( - dtype, dtype, dtype, input_shapes[0], input_shapes[0], input_shapes[0] - ): - # model that supports batching - for bs in (1, 8): - full_shapes = [ - [ - bs, - ] - + input_shape - for input_shape in input_shapes - ] - full_output_shapes = [ - [ - bs, - ] - + output_shape - for output_shape in output_shapes - ] - iu.infer_zero( - self, - "graphdef", - bs, - dtype, - full_shapes, - full_output_shapes, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) - iu.infer_zero( - self, - "savedmodel", - bs, - dtype, - full_shapes, - full_output_shapes, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) - # model that does not support batching - if no_batch: - iu.infer_zero( - self, - "graphdef_nobatch", - 1, - dtype, - input_shapes, - output_shapes, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) - iu.infer_zero( - self, - "savedmodel_nobatch", - 1, - dtype, - input_shapes, - output_shapes, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) - if tu.validate_for_onnx_model( dtype, dtype, dtype, input_shapes[0], input_shapes[0], input_shapes[0] ): diff --git a/qa/L0_infer_reshape/test.sh b/qa/L0_infer_reshape/test.sh index 218be954d9..249a1c0ca4 100755 --- a/qa/L0_infer_reshape/test.sh +++ b/qa/L0_infer_reshape/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -67,7 +67,7 @@ for i in \ zero_1_int32 \ zero_2_int32 \ zero_3_int32 ; do - cp -r models/graphdef_${i} models/custom_${i} + cp -r models/onnx_${i} models/custom_${i} rm -fr models/custom_${i}/1/* (cd models/custom_${i} && \ sed -i "s/^platform:.*/backend: \"identity\"/" config.pbtxt && \ diff --git a/qa/L0_infer_variable/infer_variable_test.py b/qa/L0_infer_variable/infer_variable_test.py index e5e6470a3c..55a2c7a084 100755 --- a/qa/L0_infer_variable/infer_variable_test.py +++ b/qa/L0_infer_variable/infer_variable_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2019-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -146,29 +146,6 @@ def _infer_exact_helper( ): ensemble_prefix.append(prefix) - if tu.validate_for_tf_model( - input_dtype, - output0_dtype, - output1_dtype, - input_shape, - output0_shape, - output1_shape, - ): - for prefix in ensemble_prefix: - for pf in ["graphdef", "savedmodel"]: - _infer_exact_helper( - self, - prefix + pf, - input_shape, - 8, - input_dtype, - output0_dtype, - output1_dtype, - output0_raw=output0_raw, - output1_raw=output1_raw, - swap=swap, - ) - if tu.validate_for_trt_model( input_dtype, output0_dtype, diff --git a/qa/L0_infer_variable/test.sh b/qa/L0_infer_variable/test.sh index 9760583b94..36d86716ad 100755 --- a/qa/L0_infer_variable/test.sh +++ b/qa/L0_infer_variable/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2019-2021, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -68,7 +68,7 @@ for TARGET in cpu gpu; do KIND="KIND_GPU" && [[ "$TARGET" == "cpu" ]] && KIND="KIND_CPU" # Onnx models are handled separately, see below - for FW in graphdef savedmodel onnx libtorch; do + for FW in onnx libtorch; do for MC in `ls models/${FW}*/config.pbtxt`; do echo "instance_group [ { kind: ${KIND} }]" >> $MC done diff --git a/qa/L0_infer_zero/infer_zero_test.py b/qa/L0_infer_zero/infer_zero_test.py index 3786c5b4a1..d05b383cff 100755 --- a/qa/L0_infer_zero/infer_zero_test.py +++ b/qa/L0_infer_zero/infer_zero_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2019-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -41,11 +41,9 @@ TEST_SYSTEM_SHARED_MEMORY = bool(int(os.environ.get("TEST_SYSTEM_SHARED_MEMORY", 0))) TEST_CUDA_SHARED_MEMORY = bool(int(os.environ.get("TEST_CUDA_SHARED_MEMORY", 0))) -BACKENDS = os.environ.get("BACKENDS", "graphdef savedmodel onnx libtorch") +BACKENDS = os.environ.get("BACKENDS", "onnx libtorch") VALIDATION_FNS = { "onnx": tu.validate_for_onnx_model, - "graphdef": tu.validate_for_tf_model, - "savedmodel": tu.validate_for_tf_model, "libtorch": tu.validate_for_libtorch_model, } diff --git a/qa/L0_inferentia_perf_analyzer/test.sh b/qa/L0_inferentia_perf_analyzer/test.sh deleted file mode 100755 index 1881e07f87..0000000000 --- a/qa/L0_inferentia_perf_analyzer/test.sh +++ /dev/null @@ -1,220 +0,0 @@ -#!/bin/bash -# Copyright (c) 2021, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -# First need to set up environment -if [ ${USE_TENSORFLOW} == "1" ] && [ ${USE_PYTORCH} == "1" ] ; then - echo " Unsupported test configuration. Only one of USE_TENSORFLOW and USE_PYTORCH can be set to 1." - exit 0 -elif [ ${USE_TENSORFLOW} == "1" ] ; then - echo "Setting up environment with tensorflow 1" - source ${TRITON_PATH}/python_backend/inferentia/scripts/setup.sh -t --tensorflow-version 1 -elif [ ${USE_PYTORCH} == "1" ] ; then - echo "Setting up environment with pytorch" - source ${TRITON_PATH}/python_backend/inferentia/scripts/setup.sh -p -else - echo " Unsupported test configuration. USE_TENSORFLOW flag is: ${USE_TENSORFLOW} and USE_PYTORCH flag is: ${USE_PYTORCH}. Only one of them can be set to 1." - exit 0 -fi -echo "done setting up environment" - -REPO_VERSION=${NVIDIA_TRITON_SERVER_VERSION} -if [ "$#" -ge 1 ]; then - REPO_VERSION=$1 -fi - -CLIENT_LOG="./perf_analyzer.log" -PERF_ANALYZER=/opt/tritonserver/qa/clients/perf_analyzer - -OUTPUT_NO_BATCH_JSONDATAFILE=${TEST_JSON_REPO}/validation_no_batch.json -OUTPUT_BATCHED_JSONDATAFILE=${TEST_JSON_REPO}/validation_batched.json -NON_ALIGNED_OUTPUT_NO_BATCH_JSONDATAFILE=${TEST_JSON_REPO}/non_aligned_validation_no_batch.json -NON_ALIGNED_OUTPUT_BATCHED_JSONDATAFILE=${TEST_JSON_REPO}/non_aligned_validation_batched.json -WRONG_OUTPUT_NO_BATCH_JSONDATAFILE=${TEST_JSON_REPO}/wrong_validation_no_batch.json -WRONG_OUTPUT_BATCHED_JSONDATAFILE=${TEST_JSON_REPO}/wrong_validation_batched.json - -ERROR_STRING="error | Request count: 0 | : 0 infer/sec" - -SERVER=/opt/tritonserver/bin/tritonserver -SERVER_LOG="./inference_server.log" -source /opt/tritonserver/qa/common/util.sh -TEST_TYPES="single multiple" -BATCHED_FLAGS="_ _batched_" -DISABLE_DEFAULT_BATCHING_FLAGS="_default_batch _no_batch" -# Helper function for clearing out existing model directories -function clear_model_dir () { - for DISABLE_DEFAULT_BATCHING_FLAG in ${DISABLE_DEFAULT_BATCHING_FLAGS}; do - for BATCHED_FLAG in ${BATCHED_FLAGS}; do - for TEST_TYPE in ${TEST_TYPES}; do - DATADIR="${TRITON_PATH}/models_${TEST_TYPE}${BATCHED_FLAG}${TEST_FRAMEWORK}${DISABLE_DEFAULT_BATCHING_FLAG}" - rm -rf DATADIR - done - done - done -} -# Helper function for generating models -function create_inferentia_models () { - for DISABLE_DEFAULT_BATCHING_FLAG in ${DISABLE_DEFAULT_BATCHING_FLAGS}; do - for BATCHED_FLAG in ${BATCHED_FLAGS}; do - for TEST_TYPE in ${TEST_TYPES}; do - CURR_GEN_SCRIPT="${GEN_SCRIPT} --model_type ${MODEL_TYPE} - --triton_model_dir ${TRITON_PATH}/models_${TEST_TYPE}${BATCHED_FLAG}${TEST_FRAMEWORK}${DISABLE_DEFAULT_BATCHING_FLAG}/add-sub-1x4 - --compiled_model ${COMPILED_MODEL}" - if [ ${DISABLE_DEFAULT_BATCHING_FLAG} == "_no_batch" ]; then - CURR_GEN_SCRIPT="${CURR_GEN_SCRIPT} - --disable_batch_requests_to_neuron" - fi - if [ ${BATCHED_FLAG} == "_batched_" ]; then - CURR_GEN_SCRIPT="${CURR_GEN_SCRIPT} - --triton_input INPUT__0,INT64,4 INPUT__1,INT64,4 - --triton_output OUTPUT__0,INT64,4 OUTPUT__1,INT64,4 - --enable_dynamic_batching - --max_batch_size 1000 - --preferred_batch_size 8 - --max_queue_delay_microseconds 100" - else - CURR_GEN_SCRIPT="${CURR_GEN_SCRIPT} - --triton_input INPUT__0,INT64,-1x4 INPUT__1,INT64,-1x4 - --triton_output OUTPUT__0,INT64,-1x4 OUTPUT__1,INT64,-1x4" - fi - if [ ${TEST_TYPE} == "single" ]; then - CURR_GEN_SCRIPT="${CURR_GEN_SCRIPT} - --neuron_core_range 0:0" - elif [ ${TEST_TYPE} == "multiple" ]; then - CURR_GEN_SCRIPT="${CURR_GEN_SCRIPT} - --triton_model_instance_count 3 - --neuron_core_range 0:7" - fi - echo ${CURR_GEN_SCRIPT} - eval ${CURR_GEN_SCRIPT} - done - done - done -} - -# Setup models -if [ ${USE_TENSORFLOW} == "1" ]; then - TEST_FRAMEWORK="tf1" - clear_model_dir - python ${TEST_JSON_REPO}/simple_model.py \ - --name add_sub_model_tf1 \ - --model_type tensorflow \ - --tf_version 1 \ - --batch_size 1 - GEN_SCRIPT="python ${TRITON_PATH}/python_backend/inferentia/scripts/gen_triton_model.py" - MODEL_TYPE="tensorflow" - COMPILED_MODEL="${PWD}/add_sub_model_tf1" - create_inferentia_models - -elif [ ${USE_PYTORCH} == "1" ]; then - TEST_FRAMEWORK="pyt" - clear_model_dir - python ${TEST_JSON_REPO}/simple_model.py \ - --name add_sub_model_pyt \ - --model_type pytorch \ - --batch_size 1 - GEN_SCRIPT="python ${TRITON_PATH}/python_backend/inferentia/scripts/gen_triton_model.py" - MODEL_TYPE="pytorch" - COMPILED_MODEL="$PWD/add_sub_model_pyt.pt" - create_inferentia_models -fi - - -RET=0 -for DISABLE_DEFAULT_BATCHING_FLAG in ${DISABLE_DEFAULT_BATCHING_FLAGS}; do - for BATCHED_FLAG in ${BATCHED_FLAGS}; do - for TEST_TYPE in $TEST_TYPES; do - DATADIR="${TRITON_PATH}/models_${TEST_TYPE}${BATCHED_FLAG}${TEST_FRAMEWORK}${DISABLE_DEFAULT_BATCHING_FLAG}" - SERVER_ARGS="--model-repository=${DATADIR} --log-verbose=1" - PERF_ANALYZER_EXTRA_ARGS="" - if [ ${BATCHED_FLAG} == "_batched_" ]; then - PERF_ANALYZER_EXTRA_ARGS="-b 6" - NON_ALIGNED_OUTPUT_JSONDATAFILE=${NON_ALIGNED_OUTPUT_BATCHED_JSONDATAFILE} - WRONG_OUTPUT_JSONDATAFILE=${WRONG_OUTPUT_BATCHED_JSONDATAFILE} - OUTPUT_JSONDATAFILE=${OUTPUT_BATCHED_JSONDATAFILE} - else - PERF_ANALYZER_EXTRA_ARGS="" - NON_ALIGNED_OUTPUT_JSONDATAFILE=${NON_ALIGNED_OUTPUT_NO_BATCH_JSONDATAFILE} - WRONG_OUTPUT_JSONDATAFILE=${WRONG_OUTPUT_NO_BATCH_JSONDATAFILE} - OUTPUT_JSONDATAFILE=${OUTPUT_NO_BATCH_JSONDATAFILE} - fi - rm -f $SERVER_LOG $CLIENT_LOG - - run_server - if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 - fi - set +e - $PERF_ANALYZER -v -m add-sub-1x4 --concurrency-range 1:10:4 --input-data=${NON_ALIGNED_OUTPUT_JSONDATAFILE} ${PERF_ANALYZER_EXTRA_ARGS} >$CLIENT_LOG 2>&1 - if [ $? -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "The 'validation_data' field doesn't align with 'data' field in the json file" | wc -l) -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - - $PERF_ANALYZER -v -m add-sub-1x4 --concurrency-range 1:10:4 --input-data=${WRONG_OUTPUT_JSONDATAFILE} ${PERF_ANALYZER_EXTRA_ARGS} >$CLIENT_LOG 2>&1 - if [ $? -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "Output doesn't match expected output" | wc -l) -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - - $PERF_ANALYZER -v -m add-sub-1x4 --concurrency-range 1:10:4 --input-data=${OUTPUT_JSONDATAFILE} ${PERF_ANALYZER_EXTRA_ARGS} >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - set -e - kill_server - done - done -done - -if [ $RET -eq 0 ]; then - echo -e "\n***\n*** Test Passed\n***" -else - echo -e "\n***\n*** Test FAILED\n***" -fi - -exit $RET diff --git a/qa/L0_input_validation/input_validation_test.py b/qa/L0_input_validation/input_validation_test.py index c65c9b6c0c..e3524247aa 100755 --- a/qa/L0_input_validation/input_validation_test.py +++ b/qa/L0_input_validation/input_validation_test.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -154,7 +154,7 @@ def test_input_shape_validation(self): def test_input_string_shape_validation(self): input_size = 16 - model_name = "graphdef_object_int32_int32" + model_name = "onnx_object_int32_int32" np_dtype_string = np.dtype(object) triton_client = tritongrpcclient.InferenceServerClient("localhost:8001") diff --git a/qa/L0_input_validation/test.sh b/qa/L0_input_validation/test.sh index 22e0560959..2d2cf515ce 100755 --- a/qa/L0_input_validation/test.sh +++ b/qa/L0_input_validation/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -124,7 +124,7 @@ dynamic_batching { } EOL -cp -r $DATADIR/qa_model_repository/graphdef_object_int32_int32 models/. +cp -r $DATADIR/qa_model_repository/onnx_object_int32_int32 models/. cp -r $DATADIR/qa_shapetensor_model_repository/plan_nobatch_zero_1_float32_int32 models/. cp -r $DATADIR/qa_shapetensor_model_repository/plan_zero_1_float32_int32 models/. @@ -151,7 +151,7 @@ kill $SERVER_PID wait $SERVER_PID # input_byte_size_test -cp -r /data/inferenceserver/${REPO_VERSION}/qa_identity_model_repository/{savedmodel_zero_1_float32,savedmodel_zero_1_object} ./models +cp -r /data/inferenceserver/${REPO_VERSION}/qa_identity_model_repository/{onnx_zero_1_float32,onnx_zero_1_object} ./models set +e LD_LIBRARY_PATH=/opt/tritonserver/lib:$LD_LIBRARY_PATH $TEST_EXEC >> $TEST_LOG 2>&1 diff --git a/qa/L0_io/test.sh b/qa/L0_io/test.sh index 0207ce1696..10318da4db 100755 --- a/qa/L0_io/test.sh +++ b/qa/L0_io/test.sh @@ -61,7 +61,7 @@ RET=0 # Prepare float32 models with basic config rm -rf $MODELSDIR -for trial in graphdef savedmodel onnx libtorch plan python python_dlpack; do +for trial in onnx libtorch plan python python_dlpack; do full=${trial}_float32_float32_float32 if [ "$trial" == "python" ]; then mkdir -p $MODELSDIR/${full}/1 && \ @@ -127,7 +127,7 @@ for trial in graphdef savedmodel onnx libtorch plan python python_dlpack; do done # Prepare string models with basic config -for trial in graphdef savedmodel onnx ; do +for trial in onnx ; do full=${trial}_object_object_object mkdir -p $MODELSDIR/${full}/1 && \ cp -r $DATADIR/${full}/1/* $MODELSDIR/${full}/1/. && \ @@ -138,9 +138,9 @@ for trial in graphdef savedmodel onnx ; do done # set up "addsub" ensemble for custom float32 model -cp -r $MODELSDIR/fan_graphdef_float32_float32_float32 $MODELSDIR/fan_${full} && \ +cp -r $MODELSDIR/fan_onnx_float32_float32_float32 $MODELSDIR/fan_${full} && \ (cd $MODELSDIR/fan_${full} && \ - sed -i "s/graphdef_float32_float32_float32/${full}/" config.pbtxt) + sed -i "s/onnx_float32_float32_float32/${full}/" config.pbtxt) # custom float32 component of ensemble cp -r $ENSEMBLEDIR/nop_TYPE_FP32_-1 $MODELSDIR/. && \ @@ -163,7 +163,7 @@ if [ $? -ne 0 ]; then fi set -e -TRIALS="graphdef savedmodel onnx libtorch plan python python_dlpack libtorch_multi_gpu libtorch_multi_device" +TRIALS="onnx libtorch plan python python_dlpack libtorch_multi_gpu libtorch_multi_device" for input_device in -1 0 1; do for output_device in -1 0 1; do for trial in ${TRIALS}; do @@ -230,7 +230,7 @@ for input_device in -1 0 1; do done done - for trial in graphdef savedmodel onnx; do + for trial in onnx; do model_devices="-1 0 1" for model_device in $model_devices; do full=${trial}_object_object_object diff --git a/qa/L0_java_resnet/ResnetTest.java b/qa/L0_java_resnet/ResnetTest.java index 37a8906930..95cf16c6f0 100644 --- a/qa/L0_java_resnet/ResnetTest.java +++ b/qa/L0_java_resnet/ResnetTest.java @@ -37,9 +37,8 @@ public class ResnetTest { // Maximum allowed difference from expected model outputs private static final float ALLOWED_DELTA = .001f; private static final String[] MODELS = { - "resnet50_fp32_libtorch", "resnet50_fp32_onnx", - // TODO: fix build to support GPU only resnet50v1.5_fp16_savedmodel - //"resnet50v1.5_fp16_savedmodel", + "resnet50_fp32_libtorch", + "resnet50_fp32_onnx", }; private static final double TRITON_MIN_COMPUTE_CAPABILITY = 7.5; private enum Backend { @@ -214,7 +213,7 @@ static void GenerateInputData(FloatPointer[] input_data) static boolean AreValidResults( String model_name, FloatPointer output, FloatPointer expected_output) { - int output_length = model_name.contains("tensorflow") ? 1001 : 1000; + int output_length = 1000; for (int i = 0; i < output_length; ++i) { float difference = output.get(i) - expected_output.get(i); if (difference > ALLOWED_DELTA) { @@ -307,9 +306,6 @@ static void Check( case ONNX: file_name += "onnx"; break; - case TF: - file_name += "tensorflow"; - break; case TORCH: file_name += "pytorch"; break; @@ -345,13 +341,11 @@ static void PerformInference( Backend backend = Backend.NONE; if (model_name.contains("onnx")) { backend = Backend.ONNX; - } else if (model_name.contains("savedmodel")) { - backend = Backend.TF; } else if (model_name.contains("torch")) { backend = Backend.TORCH; } else { FAIL( - "Supported model types (Onnx, TensorFlow, Torch) " + "Supported model types (Onnx, Torch) " + "cannot be inferred from model name " + model_name); } diff --git a/qa/L0_java_resnet/test.sh b/qa/L0_java_resnet/test.sh index 11eeeeb114..8b7e6b8a07 100755 --- a/qa/L0_java_resnet/test.sh +++ b/qa/L0_java_resnet/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -47,7 +47,6 @@ JAVACPP_BRANCH_TAG=${JAVACPP_BRANCH_TAG:="master"} # Create local model repository mkdir -p ${MODEL_REPO} -# TODO: fix build to support GPU only resnet50v1.5_fp16_savedmodel for BACKEND in _fp32_libtorch _fp32_onnx; do cp -r $DATADIR/perf_model_store/resnet50${BACKEND} ${MODEL_REPO}/ echo ${MODEL_REPO}/resnet50${BACKEND}/config.pbtxt diff --git a/qa/L0_java_sequence_batcher/SequenceTest.java b/qa/L0_java_sequence_batcher/SequenceTest.java index cfce3584de..484906f91d 100644 --- a/qa/L0_java_sequence_batcher/SequenceTest.java +++ b/qa/L0_java_sequence_batcher/SequenceTest.java @@ -251,8 +251,8 @@ static int GetExpectedResult( String model_name, int expected_result, int value, String flag) { if ((!model_name.contains("nobatch") && !model_name.contains("custom")) - || model_name.contains("graphdef") || model_name.contains("plan") - || model_name.contains("onnx") || model_name.contains("libtorch")) { + || model_name.contains("plan") || model_name.contains("onnx") + || model_name.contains("libtorch")) { expected_result = value; if (flag != null && flag.contains("start")) { expected_result++; diff --git a/qa/L0_java_sequence_batcher/test.sh b/qa/L0_java_sequence_batcher/test.sh index aadf3172be..5c0781bcda 100755 --- a/qa/L0_java_sequence_batcher/test.sh +++ b/qa/L0_java_sequence_batcher/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -63,7 +63,7 @@ sed -i 's/Simple/SequenceTest/g' $SAMPLES_REPO/pom.xml rm -f *.log RET=0 -for BACKEND in graphdef libtorch onnx savedmodel; do +for BACKEND in libtorch onnx; do # Create local model repository mkdir -p ${MODEL_REPO} MODEL=${BACKEND}_nobatch_sequence_int32 diff --git a/qa/L0_java_simple_example/test.sh b/qa/L0_java_simple_example/test.sh index 3b3fa41f0b..0441e88641 100755 --- a/qa/L0_java_simple_example/test.sh +++ b/qa/L0_java_simple_example/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -104,7 +104,7 @@ function run_cpu_tests_int32() { } function run_cpu_tests_fp32() { - for trial in graphdef savedmodel; do + for trial in onnx; do full=${trial}_float32_float32_float32 set +e rm -rf ${MODEL_REPO} diff --git a/qa/L0_large_payload/large_payload_test.py b/qa/L0_large_payload/large_payload_test.py index f9c0a49dfd..1ab6d6e340 100755 --- a/qa/L0_large_payload/large_payload_test.py +++ b/qa/L0_large_payload/large_payload_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2019-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -134,20 +134,6 @@ def _test_helper( "output is different from input", ) - def test_graphdef(self): - # graphdef_nobatch_zero_1_float32 is identity model with input shape [-1] - for client in self._clients: - model_name = tu.get_zero_model_name("graphdef_nobatch", 1, self._data_type) - self._test_helper(client, model_name) - - def test_savedmodel(self): - # savedmodel_nobatch_zero_1_float32 is identity model with input shape [-1] - for client in self._clients: - model_name = tu.get_zero_model_name( - "savedmodel_nobatch", 1, self._data_type - ) - self._test_helper(client, model_name) - def test_onnx(self): # onnx_nobatch_zero_1_float32 is identity model with input shape [-1] for client in self._clients: diff --git a/qa/L0_large_payload/test.sh b/qa/L0_large_payload/test.sh index 325cab4ed5..b4aff503d1 100755 --- a/qa/L0_large_payload/test.sh +++ b/qa/L0_large_payload/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2019-2021, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -56,7 +56,7 @@ RET=0 MODEL_SUFFIX=nobatch_zero_1_float32 rm -fr all_models && mkdir all_models -for TARGET in graphdef savedmodel onnx libtorch plan; do +for TARGET in onnx libtorch plan; do cp -r /data/inferenceserver/${REPO_VERSION}/qa_identity_model_repository/${TARGET}_$MODEL_SUFFIX \ all_models/. done @@ -71,7 +71,7 @@ cp ../python_models/identity_fp32/model.py all_models/python_$MODEL_SUFFIX/1/mod # Restart server before every test to make sure server state # is invariant to previous test -for TARGET in graphdef savedmodel onnx libtorch plan python; do +for TARGET in onnx libtorch plan python; do rm -fr models && mkdir models && \ cp -r all_models/${TARGET}_$MODEL_SUFFIX models/. diff --git a/qa/L0_lifecycle/lifecycle_test.py b/qa/L0_lifecycle/lifecycle_test.py index 1dc659e27c..8ad95e3d90 100755 --- a/qa/L0_lifecycle/lifecycle_test.py +++ b/qa/L0_lifecycle/lifecycle_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2018-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -172,7 +172,7 @@ def test_parse_error_modelfail(self): # Server was started but with a model that fails to load try: model_name = tu.get_model_name( - "graphdef", np.float32, np.float32, np.float32 + "libtorch", np.float32, np.float32, np.float32 ) triton_client = grpcclient.InferenceServerClient( @@ -194,18 +194,18 @@ def test_parse_error_modelfail(self): # Inferencing with the missing model should fail. try: iu.infer_exact( - self, "graphdef", tensor_shape, 1, np.float32, np.float32, np.float32 + self, "libtorch", tensor_shape, 1, np.float32, np.float32, np.float32 ) self.assertTrue(False, "expected error for unavailable model " + model_name) except Exception as ex: self.assertIn( - "Request for unknown model: 'graphdef_float32_float32_float32' has no available versions", + "Request for unknown model: 'libtorch_float32_float32_float32' has no available versions", ex.message(), ) # And other models should be loaded successfully try: - for base_name in ["savedmodel", "onnx"]: + for base_name in ["openvino", "onnx"]: for triton_client in ( httpclient.InferenceServerClient("localhost:8000", verbose=True), grpcclient.InferenceServerClient("localhost:8001", verbose=True), @@ -235,7 +235,7 @@ def test_parse_error_modelfail_nostrict(self): # Server was started but with a model that fails to load try: model_name = tu.get_model_name( - "graphdef", np.float32, np.float32, np.float32 + "libtorch", np.float32, np.float32, np.float32 ) triton_client = grpcclient.InferenceServerClient( @@ -257,18 +257,18 @@ def test_parse_error_modelfail_nostrict(self): # Inferencing with the missing model should fail. try: iu.infer_exact( - self, "graphdef", tensor_shape, 1, np.float32, np.float32, np.float32 + self, "libtorch", tensor_shape, 1, np.float32, np.float32, np.float32 ) self.assertTrue(False, "expected error for unavailable model " + model_name) except Exception as ex: self.assertIn( - "Request for unknown model: 'graphdef_float32_float32_float32' has no available versions", + "Request for unknown model: 'libtorch_float32_float32_float32' has no available versions", ex.message(), ) # And other models should be loaded successfully try: - for base_name in ["savedmodel", "onnx"]: + for base_name in ["openvino", "onnx"]: for triton_client in ( httpclient.InferenceServerClient("localhost:8000", verbose=True), grpcclient.InferenceServerClient("localhost:8001", verbose=True), @@ -301,7 +301,7 @@ def test_parse_error_no_model_config(self): ): try: model_name = tu.get_model_name( - "graphdef", np.float32, np.float32, np.float32 + "openvino", np.float32, np.float32, np.float32 ) # expecting ready because not strict readiness @@ -318,13 +318,13 @@ def test_parse_error_no_model_config(self): except Exception as ex: self.assertIn( - "Request for unknown model: 'graphdef_float32_float32_float32' is not found", + "Request for unknown model: 'openvino_float32_float32_float32' is not found", ex.message(), ) # And other models should be loaded successfully try: - for base_name in ["savedmodel", "onnx"]: + for base_name in ["libtorch", "onnx"]: model_name = tu.get_model_name( base_name, np.float32, np.float32, np.float32 ) @@ -365,7 +365,7 @@ def test_init_error_modelfail(self): # And other models should be loaded successfully try: - for base_name in ["graphdef", "savedmodel", "onnx"]: + for base_name in ["openvino", "libtorch", "onnx"]: model_name = tu.get_model_name( base_name, np.float32, np.float32, np.float32 ) @@ -375,7 +375,7 @@ def test_init_error_modelfail(self): try: tensor_shape = (1, 16) - for base_name in ["graphdef", "savedmodel", "onnx"]: + for base_name in ["openvino", "libtorch", "onnx"]: iu.infer_exact( self, base_name, @@ -403,7 +403,7 @@ def test_parse_error_model_no_version(self): self.assertFalse(triton_client.is_server_ready()) model_name = tu.get_model_name( - "graphdef", np.float32, np.float32, np.float32 + "openvino", np.float32, np.float32, np.float32 ) self.assertFalse(triton_client.is_model_ready(model_name)) except Exception as ex: @@ -411,7 +411,7 @@ def test_parse_error_model_no_version(self): # Sanity check that other models are loaded properly try: - for base_name in ["savedmodel", "onnx"]: + for base_name in ["libtorch", "onnx"]: model_name = tu.get_model_name( base_name, np.float32, np.float32, np.float32 ) @@ -425,7 +425,7 @@ def test_parse_error_model_no_version(self): self.assertTrue(False, "unexpected error {}".format(ex)) try: - for base_name in ["savedmodel", "onnx"]: + for base_name in ["libtorch", "onnx"]: iu.infer_exact( self, base_name, @@ -453,12 +453,12 @@ def test_parse_error_model_no_version(self): try: iu.infer_exact( - self, "graphdef", tensor_shape, 1, np.float32, np.float32, np.float32 + self, "openvino", tensor_shape, 1, np.float32, np.float32, np.float32 ) self.assertTrue(False, "expected error for unavailable model " + model_name) except Exception as ex: self.assertIn( - "Request for unknown model: 'graphdef_float32_float32_float32' has no available versions", + "Request for unknown model: 'openvino_float32_float32_float32' has no available versions", ex.message(), ) @@ -475,7 +475,7 @@ def test_parse_ignore_zero_prefixed_version(self): self.assertTrue(triton_client.is_server_ready()) model_name = tu.get_model_name( - "savedmodel", np.float32, np.float32, np.float32 + "libtorch", np.float32, np.float32, np.float32 ) self.assertTrue(triton_client.is_model_ready(model_name, "1")) except Exception as ex: @@ -485,7 +485,7 @@ def test_parse_ignore_zero_prefixed_version(self): # swap=False for version 1 iu.infer_exact( self, - "savedmodel", + "libtorch", tensor_shape, 1, np.float32, @@ -509,7 +509,7 @@ def test_parse_ignore_non_intergral_version(self): self.assertTrue(triton_client.is_server_ready()) model_name = tu.get_model_name( - "savedmodel", np.float32, np.float32, np.float32 + "libtorch", np.float32, np.float32, np.float32 ) self.assertTrue(triton_client.is_model_ready(model_name, "1")) except Exception as ex: @@ -519,7 +519,7 @@ def test_parse_ignore_non_intergral_version(self): # swap=False for version 1 iu.infer_exact( self, - "savedmodel", + "libtorch", tensor_shape, 1, np.float32, @@ -532,12 +532,12 @@ def test_parse_ignore_non_intergral_version(self): def test_dynamic_model_load_unload(self): tensor_shape = (1, 16) - savedmodel_name = tu.get_model_name( - "savedmodel", np.float32, np.float32, np.float32 + libtorch_name = tu.get_model_name( + "libtorch", np.float32, np.float32, np.float32 ) onnx_name = tu.get_model_name("onnx", np.float32, np.float32, np.float32) - # Make sure savedmodel model is not in the status (because + # Make sure libtorch model is not in the status (because # initially it is not in the model repository) for triton_client in ( httpclient.InferenceServerClient("localhost:8000", verbose=True), @@ -546,17 +546,17 @@ def test_dynamic_model_load_unload(self): try: self.assertTrue(triton_client.is_server_live()) self.assertTrue(triton_client.is_server_ready()) - self.assertFalse(triton_client.is_model_ready(savedmodel_name, "1")) - self.assertFalse(triton_client.is_model_ready(savedmodel_name, "3")) + self.assertFalse(triton_client.is_model_ready(libtorch_name, "1")) + self.assertFalse(triton_client.is_model_ready(libtorch_name, "3")) self.assertTrue(triton_client.is_model_ready(onnx_name, "1")) self.assertTrue(triton_client.is_model_ready(onnx_name, "3")) except Exception as ex: self.assertTrue(False, "unexpected error {}".format(ex)) - # Add savedmodel model to the model repository and give it time to + # Add libtorch model to the model repository and give it time to # load. Make sure that it has a status and is ready. try: - shutil.copytree(savedmodel_name, "models/" + savedmodel_name) + shutil.copytree(libtorch_name, "models/" + libtorch_name) time.sleep(5) # wait for model to load for triton_client in ( httpclient.InferenceServerClient("localhost:8000", verbose=True), @@ -564,8 +564,8 @@ def test_dynamic_model_load_unload(self): ): self.assertTrue(triton_client.is_server_live()) self.assertTrue(triton_client.is_server_ready()) - self.assertTrue(triton_client.is_model_ready(savedmodel_name, "1")) - self.assertTrue(triton_client.is_model_ready(savedmodel_name, "3")) + self.assertTrue(triton_client.is_model_ready(libtorch_name, "1")) + self.assertTrue(triton_client.is_model_ready(libtorch_name, "3")) self.assertTrue(triton_client.is_model_ready(onnx_name, "1")) self.assertTrue(triton_client.is_model_ready(onnx_name, "3")) except Exception as ex: @@ -575,7 +575,7 @@ def test_dynamic_model_load_unload(self): try: iu.infer_exact( self, - "savedmodel", + "libtorch", tensor_shape, 1, np.float32, @@ -586,15 +586,15 @@ def test_dynamic_model_load_unload(self): except Exception as ex: self.assertTrue(False, "unexpected error {}".format(ex)) - # Make sure savedmodel has execution stats + # Make sure libtorch has execution stats try: triton_client = httpclient.InferenceServerClient( "localhost:8000", verbose=True ) - stats = triton_client.get_inference_statistics(savedmodel_name) + stats = triton_client.get_inference_statistics(libtorch_name) self.assertEqual(len(stats["model_stats"]), 2) for idx in range(len(stats["model_stats"])): - self.assertEqual(stats["model_stats"][idx]["name"], savedmodel_name) + self.assertEqual(stats["model_stats"][idx]["name"], libtorch_name) if stats["model_stats"][idx]["version"] == "1": self.assertEqual( stats["model_stats"][idx]["inference_stats"]["success"][ @@ -613,10 +613,10 @@ def test_dynamic_model_load_unload(self): triton_client = grpcclient.InferenceServerClient( "localhost:8001", verbose=True ) - stats = triton_client.get_inference_statistics(savedmodel_name) + stats = triton_client.get_inference_statistics(libtorch_name) self.assertEqual(len(stats.model_stats), 2) for idx in range(len(stats.model_stats)): - self.assertEqual(stats.model_stats[idx].name, savedmodel_name) + self.assertEqual(stats.model_stats[idx].name, libtorch_name) if stats.model_stats[idx].version == "1": self.assertEqual( stats.model_stats[idx].inference_stats.success.count, 0 @@ -629,10 +629,10 @@ def test_dynamic_model_load_unload(self): except Exception as ex: self.assertTrue(False, "unexpected error {}".format(ex)) - # Remove savedmodel model from the model repository and give it + # Remove libtorch model from the model repository and give it # time to unload. Make sure that it is no longer available. try: - shutil.rmtree("models/" + savedmodel_name) + shutil.rmtree("models/" + libtorch_name) time.sleep(5) # wait for model to unload for triton_client in ( httpclient.InferenceServerClient("localhost:8000", verbose=True), @@ -640,8 +640,8 @@ def test_dynamic_model_load_unload(self): ): self.assertTrue(triton_client.is_server_live()) self.assertTrue(triton_client.is_server_ready()) - self.assertFalse(triton_client.is_model_ready(savedmodel_name, "1")) - self.assertFalse(triton_client.is_model_ready(savedmodel_name, "3")) + self.assertFalse(triton_client.is_model_ready(libtorch_name, "1")) + self.assertFalse(triton_client.is_model_ready(libtorch_name, "3")) self.assertTrue(triton_client.is_model_ready(onnx_name, "1")) self.assertTrue(triton_client.is_model_ready(onnx_name, "3")) except Exception as ex: @@ -651,7 +651,7 @@ def test_dynamic_model_load_unload(self): try: iu.infer_exact( self, - "savedmodel", + "libtorch", tensor_shape, 1, np.float32, @@ -660,19 +660,19 @@ def test_dynamic_model_load_unload(self): swap=True, ) self.assertTrue( - False, "expected error for unavailable model " + savedmodel_name + False, "expected error for unavailable model " + libtorch_name ) except Exception as ex: self.assertIn( "Request for unknown model: '{}' has no available versions".format( - savedmodel_name + libtorch_name ), ex.message(), ) # Add back the same model. The status/stats should be reset. try: - shutil.copytree(savedmodel_name, "models/" + savedmodel_name) + shutil.copytree(libtorch_name, "models/" + libtorch_name) time.sleep(5) # wait for model to load for triton_client in ( httpclient.InferenceServerClient("localhost:8000", verbose=True), @@ -680,18 +680,18 @@ def test_dynamic_model_load_unload(self): ): self.assertTrue(triton_client.is_server_live()) self.assertTrue(triton_client.is_server_ready()) - self.assertTrue(triton_client.is_model_ready(savedmodel_name, "1")) - self.assertTrue(triton_client.is_model_ready(savedmodel_name, "3")) + self.assertTrue(triton_client.is_model_ready(libtorch_name, "1")) + self.assertTrue(triton_client.is_model_ready(libtorch_name, "3")) self.assertTrue(triton_client.is_model_ready(onnx_name, "1")) self.assertTrue(triton_client.is_model_ready(onnx_name, "3")) triton_client = httpclient.InferenceServerClient( "localhost:8000", verbose=True ) - stats = triton_client.get_inference_statistics(savedmodel_name) + stats = triton_client.get_inference_statistics(libtorch_name) self.assertEqual(len(stats["model_stats"]), 2) - self.assertEqual(stats["model_stats"][0]["name"], savedmodel_name) - self.assertEqual(stats["model_stats"][1]["name"], savedmodel_name) + self.assertEqual(stats["model_stats"][0]["name"], libtorch_name) + self.assertEqual(stats["model_stats"][1]["name"], libtorch_name) self.assertEqual( stats["model_stats"][0]["inference_stats"]["success"]["count"], 0 ) @@ -702,10 +702,10 @@ def test_dynamic_model_load_unload(self): triton_client = grpcclient.InferenceServerClient( "localhost:8001", verbose=True ) - stats = triton_client.get_inference_statistics(savedmodel_name) + stats = triton_client.get_inference_statistics(libtorch_name) self.assertEqual(len(stats.model_stats), 2) - self.assertEqual(stats.model_stats[0].name, savedmodel_name) - self.assertEqual(stats.model_stats[1].name, savedmodel_name) + self.assertEqual(stats.model_stats[0].name, libtorch_name) + self.assertEqual(stats.model_stats[1].name, libtorch_name) self.assertEqual(stats.model_stats[0].inference_stats.success.count, 0) self.assertEqual(stats.model_stats[1].inference_stats.success.count, 0) @@ -723,8 +723,8 @@ def test_dynamic_model_load_unload(self): ): self.assertTrue(triton_client.is_server_live()) self.assertTrue(triton_client.is_server_ready()) - self.assertTrue(triton_client.is_model_ready(savedmodel_name, "1")) - self.assertTrue(triton_client.is_model_ready(savedmodel_name, "3")) + self.assertTrue(triton_client.is_model_ready(libtorch_name, "1")) + self.assertTrue(triton_client.is_model_ready(libtorch_name, "3")) self.assertFalse(triton_client.is_model_ready(onnx_name, "1")) self.assertFalse(triton_client.is_model_ready(onnx_name, "3")) except Exception as ex: @@ -751,12 +751,12 @@ def test_dynamic_model_load_unload(self): def test_dynamic_model_load_unload_disabled(self): tensor_shape = (1, 16) - savedmodel_name = tu.get_model_name( - "savedmodel", np.float32, np.float32, np.float32 + libtorch_name = tu.get_model_name( + "libtorch", np.float32, np.float32, np.float32 ) onnx_name = tu.get_model_name("onnx", np.float32, np.float32, np.float32) - # Make sure savedmodel model is not in the status (because + # Make sure libtorch model is not in the status (because # initially it is not in the model repository) for triton_client in ( httpclient.InferenceServerClient("localhost:8000", verbose=True), @@ -765,17 +765,17 @@ def test_dynamic_model_load_unload_disabled(self): try: self.assertTrue(triton_client.is_server_live()) self.assertTrue(triton_client.is_server_ready()) - self.assertFalse(triton_client.is_model_ready(savedmodel_name, "1")) - self.assertFalse(triton_client.is_model_ready(savedmodel_name, "3")) + self.assertFalse(triton_client.is_model_ready(libtorch_name, "1")) + self.assertFalse(triton_client.is_model_ready(libtorch_name, "3")) self.assertTrue(triton_client.is_model_ready(onnx_name, "1")) self.assertTrue(triton_client.is_model_ready(onnx_name, "3")) except Exception as ex: self.assertTrue(False, "unexpected error {}".format(ex)) - # Add savedmodel model to the model repository and give it time to + # Add libtorch model to the model repository and give it time to # load. But it shouldn't load because dynamic loading is disabled. try: - shutil.copytree(savedmodel_name, "models/" + savedmodel_name) + shutil.copytree(libtorch_name, "models/" + libtorch_name) time.sleep(5) # wait for model to load for triton_client in ( httpclient.InferenceServerClient("localhost:8000", verbose=True), @@ -783,8 +783,8 @@ def test_dynamic_model_load_unload_disabled(self): ): self.assertTrue(triton_client.is_server_live()) self.assertTrue(triton_client.is_server_ready()) - self.assertFalse(triton_client.is_model_ready(savedmodel_name, "1")) - self.assertFalse(triton_client.is_model_ready(savedmodel_name, "3")) + self.assertFalse(triton_client.is_model_ready(libtorch_name, "1")) + self.assertFalse(triton_client.is_model_ready(libtorch_name, "3")) self.assertTrue(triton_client.is_model_ready(onnx_name, "1")) self.assertTrue(triton_client.is_model_ready(onnx_name, "3")) except Exception as ex: @@ -794,7 +794,7 @@ def test_dynamic_model_load_unload_disabled(self): try: iu.infer_exact( self, - "savedmodel", + "libtorch", tensor_shape, 1, np.float32, @@ -803,11 +803,11 @@ def test_dynamic_model_load_unload_disabled(self): swap=True, ) self.assertTrue( - False, "expected error for unavailable model " + savedmodel_name + False, "expected error for unavailable model " + libtorch_name ) except Exception as ex: self.assertIn( - "Request for unknown model: 'savedmodel_float32_float32_float32' is not found", + "Request for unknown model: 'libtorch_float32_float32_float32' is not found", ex.message(), ) @@ -822,8 +822,8 @@ def test_dynamic_model_load_unload_disabled(self): ): self.assertTrue(triton_client.is_server_live()) self.assertTrue(triton_client.is_server_ready()) - self.assertFalse(triton_client.is_model_ready(savedmodel_name, "1")) - self.assertFalse(triton_client.is_model_ready(savedmodel_name, "3")) + self.assertFalse(triton_client.is_model_ready(libtorch_name, "1")) + self.assertFalse(triton_client.is_model_ready(libtorch_name, "3")) self.assertTrue(triton_client.is_model_ready(onnx_name, "1")) self.assertTrue(triton_client.is_model_ready(onnx_name, "3")) except Exception as ex: @@ -847,7 +847,7 @@ def test_dynamic_model_load_unload_disabled(self): def test_dynamic_version_load_unload(self): tensor_shape = (1, 16) - graphdef_name = tu.get_model_name("graphdef", np.int32, np.int32, np.int32) + libtorch_name = tu.get_model_name("libtorch", np.int32, np.int32, np.int32) # There are 3 versions. Make sure that all have status and are # ready. @@ -858,9 +858,9 @@ def test_dynamic_version_load_unload(self): ): self.assertTrue(triton_client.is_server_live()) self.assertTrue(triton_client.is_server_ready()) - self.assertTrue(triton_client.is_model_ready(graphdef_name, "1")) - self.assertTrue(triton_client.is_model_ready(graphdef_name, "2")) - self.assertTrue(triton_client.is_model_ready(graphdef_name, "3")) + self.assertTrue(triton_client.is_model_ready(libtorch_name, "1")) + self.assertTrue(triton_client.is_model_ready(libtorch_name, "2")) + self.assertTrue(triton_client.is_model_ready(libtorch_name, "3")) except Exception as ex: self.assertTrue(False, "unexpected error {}".format(ex)) @@ -868,7 +868,7 @@ def test_dynamic_version_load_unload(self): try: iu.infer_exact( self, - "graphdef", + "libtorch", tensor_shape, 1, np.int32, @@ -885,10 +885,10 @@ def test_dynamic_version_load_unload(self): triton_client = httpclient.InferenceServerClient( "localhost:8000", verbose=True ) - stats = triton_client.get_inference_statistics(graphdef_name) + stats = triton_client.get_inference_statistics(libtorch_name) self.assertEqual(len(stats["model_stats"]), 3) for idx in range(len(stats["model_stats"])): - self.assertEqual(stats["model_stats"][idx]["name"], graphdef_name) + self.assertEqual(stats["model_stats"][idx]["name"], libtorch_name) if stats["model_stats"][idx]["version"] == "1": self.assertNotEqual( stats["model_stats"][idx]["inference_stats"]["success"][ @@ -907,10 +907,10 @@ def test_dynamic_version_load_unload(self): triton_client = grpcclient.InferenceServerClient( "localhost:8001", verbose=True ) - stats = triton_client.get_inference_statistics(graphdef_name) + stats = triton_client.get_inference_statistics(libtorch_name) self.assertEqual(len(stats.model_stats), 3) for idx in range(len(stats.model_stats)): - self.assertEqual(stats.model_stats[idx].name, graphdef_name) + self.assertEqual(stats.model_stats[idx].name, libtorch_name) if stats.model_stats[idx].version == "1": self.assertNotEqual( stats.model_stats[idx].inference_stats.success.count, 0 @@ -926,7 +926,7 @@ def test_dynamic_version_load_unload(self): # Remove version 1 from the model repository and give it time to # unload. Make sure that it is unavailable. try: - shutil.rmtree("models/" + graphdef_name + "/1") + shutil.rmtree("models/" + libtorch_name + "/1") time.sleep(5) # wait for version to unload for triton_client in ( httpclient.InferenceServerClient("localhost:8000", verbose=True), @@ -934,9 +934,9 @@ def test_dynamic_version_load_unload(self): ): self.assertTrue(triton_client.is_server_live()) self.assertTrue(triton_client.is_server_ready()) - self.assertFalse(triton_client.is_model_ready(graphdef_name, "1")) - self.assertTrue(triton_client.is_model_ready(graphdef_name, "2")) - self.assertTrue(triton_client.is_model_ready(graphdef_name, "3")) + self.assertFalse(triton_client.is_model_ready(libtorch_name, "1")) + self.assertTrue(triton_client.is_model_ready(libtorch_name, "2")) + self.assertTrue(triton_client.is_model_ready(libtorch_name, "3")) except Exception as ex: self.assertTrue(False, "unexpected error {}".format(ex)) @@ -944,7 +944,7 @@ def test_dynamic_version_load_unload(self): try: iu.infer_exact( self, - "graphdef", + "libtorch", tensor_shape, 1, np.int32, @@ -954,18 +954,18 @@ def test_dynamic_version_load_unload(self): model_version=1, ) self.assertTrue( - False, "expected error for unavailable model " + graphdef_name + False, "expected error for unavailable model " + libtorch_name ) except Exception as ex: self.assertIn( - "Request for unknown model: 'graphdef_int32_int32_int32' version 1 is not at ready state", + "Request for unknown model: 'libtorch_int32_int32_int32' version 1 is not at ready state", ex.message(), ) # Add another version to the model repository. try: shutil.copytree( - "models/" + graphdef_name + "/2", "models/" + graphdef_name + "/7" + "models/" + libtorch_name + "/2", "models/" + libtorch_name + "/7" ) time.sleep(5) # wait for version to load for triton_client in ( @@ -974,23 +974,23 @@ def test_dynamic_version_load_unload(self): ): self.assertTrue(triton_client.is_server_live()) self.assertTrue(triton_client.is_server_ready()) - self.assertFalse(triton_client.is_model_ready(graphdef_name, "1")) - self.assertTrue(triton_client.is_model_ready(graphdef_name, "2")) - self.assertTrue(triton_client.is_model_ready(graphdef_name, "3")) - self.assertTrue(triton_client.is_model_ready(graphdef_name, "7")) + self.assertFalse(triton_client.is_model_ready(libtorch_name, "1")) + self.assertTrue(triton_client.is_model_ready(libtorch_name, "2")) + self.assertTrue(triton_client.is_model_ready(libtorch_name, "3")) + self.assertTrue(triton_client.is_model_ready(libtorch_name, "7")) except Exception as ex: self.assertTrue(False, "unexpected error {}".format(ex)) def test_dynamic_version_load_unload_disabled(self): tensor_shape = (1, 16) - graphdef_name = tu.get_model_name("graphdef", np.int32, np.int32, np.int32) + libtorch_name = tu.get_model_name("libtorch", np.int32, np.int32, np.int32) # Add a new version to the model repository and give it time to # load. But it shouldn't load because dynamic loading is # disabled. try: shutil.copytree( - "models/" + graphdef_name + "/2", "models/" + graphdef_name + "/7" + "models/" + libtorch_name + "/2", "models/" + libtorch_name + "/7" ) time.sleep(5) # wait for model to load for triton_client in ( @@ -999,10 +999,10 @@ def test_dynamic_version_load_unload_disabled(self): ): self.assertTrue(triton_client.is_server_live()) self.assertTrue(triton_client.is_server_ready()) - self.assertTrue(triton_client.is_model_ready(graphdef_name, "1")) - self.assertTrue(triton_client.is_model_ready(graphdef_name, "2")) - self.assertTrue(triton_client.is_model_ready(graphdef_name, "3")) - self.assertFalse(triton_client.is_model_ready(graphdef_name, "7")) + self.assertTrue(triton_client.is_model_ready(libtorch_name, "1")) + self.assertTrue(triton_client.is_model_ready(libtorch_name, "2")) + self.assertTrue(triton_client.is_model_ready(libtorch_name, "3")) + self.assertFalse(triton_client.is_model_ready(libtorch_name, "7")) except Exception as ex: self.assertTrue(False, "unexpected error {}".format(ex)) @@ -1010,7 +1010,7 @@ def test_dynamic_version_load_unload_disabled(self): # Unloading is disabled so it should remain available # in the status. try: - shutil.rmtree("models/" + graphdef_name + "/1") + shutil.rmtree("models/" + libtorch_name + "/1") time.sleep(5) # wait for version to unload (but it shouldn't) for triton_client in ( httpclient.InferenceServerClient("localhost:8000", verbose=True), @@ -1018,10 +1018,10 @@ def test_dynamic_version_load_unload_disabled(self): ): self.assertTrue(triton_client.is_server_live()) self.assertTrue(triton_client.is_server_ready()) - self.assertTrue(triton_client.is_model_ready(graphdef_name, "1")) - self.assertTrue(triton_client.is_model_ready(graphdef_name, "2")) - self.assertTrue(triton_client.is_model_ready(graphdef_name, "3")) - self.assertFalse(triton_client.is_model_ready(graphdef_name, "7")) + self.assertTrue(triton_client.is_model_ready(libtorch_name, "1")) + self.assertTrue(triton_client.is_model_ready(libtorch_name, "2")) + self.assertTrue(triton_client.is_model_ready(libtorch_name, "3")) + self.assertFalse(triton_client.is_model_ready(libtorch_name, "7")) except Exception as ex: self.assertTrue(False, "unexpected error {}".format(ex)) @@ -1030,7 +1030,7 @@ def test_dynamic_version_load_unload_disabled(self): try: iu.infer_exact( self, - "graphdef", + "libtorch", tensor_shape, 1, np.int32, @@ -1043,13 +1043,13 @@ def test_dynamic_version_load_unload_disabled(self): self.assertTrue(False, "unexpected error {}".format(ex)) def test_dynamic_model_modify(self): - models_base = ("savedmodel", "plan") + models_base = ("libtorch", "plan") models_shape = ((1, 16), (1, 16)) models = list() for m in models_base: models.append(tu.get_model_name(m, np.float32, np.float32, np.float32)) - # Make sure savedmodel and plan are in the status + # Make sure libtorch and plan are in the status for model_name in models: try: for triton_client in ( @@ -1170,13 +1170,13 @@ def test_dynamic_model_modify(self): self.assertTrue(False, "unexpected error {}".format(ex)) def test_dynamic_file_delete(self): - models_base = ("savedmodel", "plan") + models_base = ("onnx", "plan") models_shape = ((1, 16), (1, 16)) models = list() for m in models_base: models.append(tu.get_model_name(m, np.float32, np.float32, np.float32)) - # Make sure savedmodel and plan are in the status + # Make sure onnx and plan are in the status for model_name in models: try: for triton_client in ( @@ -1266,24 +1266,24 @@ def test_dynamic_file_delete(self): def test_multiple_model_repository_polling(self): model_shape = (1, 16) - savedmodel_name = tu.get_model_name( - "savedmodel", np.float32, np.float32, np.float32 + libtorch_name = tu.get_model_name( + "libtorch", np.float32, np.float32, np.float32 ) # Models should be loaded successfully and infer - # successfully. Initially savedmodel only has version 1. + # successfully. Initially libtorch only has version 1. self._infer_success_models( [ - "savedmodel", + "libtorch", ], (1,), model_shape, ) - self._infer_success_models(["graphdef", "onnx"], (1, 3), model_shape) + self._infer_success_models(["openvino", "onnx"], (1, 3), model_shape) - # Add the savedmodel to the second model repository, should cause + # Add the libtorch to the second model repository, should cause # it to be unloaded due to duplication - shutil.copytree(savedmodel_name, "models_0/" + savedmodel_name) + shutil.copytree(libtorch_name, "models_0/" + libtorch_name) time.sleep(5) # wait for models to reload try: for triton_client in ( @@ -1292,31 +1292,31 @@ def test_multiple_model_repository_polling(self): ): self.assertTrue(triton_client.is_server_live()) self.assertTrue(triton_client.is_server_ready()) - self.assertFalse(triton_client.is_model_ready(savedmodel_name, "1")) - self.assertFalse(triton_client.is_model_ready(savedmodel_name, "3")) + self.assertFalse(triton_client.is_model_ready(libtorch_name, "1")) + self.assertFalse(triton_client.is_model_ready(libtorch_name, "3")) except Exception as ex: self.assertTrue(False, "unexpected error {}".format(ex)) - self._infer_success_models(["graphdef", "onnx"], (1, 3), model_shape) + self._infer_success_models(["openvino", "onnx"], (1, 3), model_shape) - # Remove the savedmodel from the first model repository, the + # Remove the libtorch from the first model repository, the # model from the second model repository should be loaded - # properly. In the second model repository savedmodel should + # properly. In the second model repository libtorch should # have versions 1 and 3. - shutil.rmtree("models/" + savedmodel_name) + shutil.rmtree("models/" + libtorch_name) time.sleep(5) # wait for model to unload self._infer_success_models( - ["savedmodel", "graphdef", "onnx"], (1, 3), model_shape + ["libtorch", "openvino", "onnx"], (1, 3), model_shape ) def test_multiple_model_repository_control(self): # similar to test_multiple_model_repository_polling, but the # model load/unload is controlled by the API model_shape = (1, 16) - savedmodel_name = tu.get_model_name( - "savedmodel", np.float32, np.float32, np.float32 + libtorch_name = tu.get_model_name( + "libtorch", np.float32, np.float32, np.float32 ) - model_bases = ["savedmodel", "graphdef", "onnx"] + model_bases = ["libtorch", "openvino", "onnx"] # Initially models are not loaded for base in model_bases: @@ -1345,38 +1345,38 @@ def test_multiple_model_repository_control(self): self.assertTrue(False, "unexpected error {}".format(ex)) # Models should be loaded successfully and infer - # successfully. Initially savedmodel only has version 1. + # successfully. Initially libtorch only has version 1. self._infer_success_models( [ - "savedmodel", + "libtorch", ], (1,), model_shape, ) - self._infer_success_models(["graphdef", "onnx"], (1, 3), model_shape) + self._infer_success_models(["openvino", "onnx"], (1, 3), model_shape) - # Add the savedmodel to the second model repository. Because + # Add the libtorch to the second model repository. Because # not polling this doesn't change any model state, all models # are still loaded and available. - shutil.copytree(savedmodel_name, "models_0/" + savedmodel_name) + shutil.copytree(libtorch_name, "models_0/" + libtorch_name) self._infer_success_models( [ - "savedmodel", + "libtorch", ], (1,), model_shape, ) - self._infer_success_models(["graphdef", "onnx"], (1, 3), model_shape) + self._infer_success_models(["openvino", "onnx"], (1, 3), model_shape) - # Load savedmodel again which should fail because it is now duplicated + # Load libtorch again which should fail because it is now duplicated # in 2 model repositories. Use HTTP here. try: triton_client = httpclient.InferenceServerClient( "localhost:8000", verbose=True ) - triton_client.load_model(savedmodel_name) + triton_client.load_model(libtorch_name) except Exception as ex: - self.assertIn("failed to load '{}'".format(savedmodel_name), ex.message()) + self.assertIn("failed to load '{}'".format(libtorch_name), ex.message()) try: for triton_client in ( @@ -1387,33 +1387,33 @@ def test_multiple_model_repository_control(self): self.assertTrue(triton_client.is_server_ready()) # Unlike polling mode, the failed load on the duplicate model # should NOT unload the existing versions in model control mode. - self.assertTrue(triton_client.is_model_ready(savedmodel_name, "1")) + self.assertTrue(triton_client.is_model_ready(libtorch_name, "1")) # Version 3 did not exist in the first model repository, so # it should still not be loaded. - self.assertFalse(triton_client.is_model_ready(savedmodel_name, "3")) + self.assertFalse(triton_client.is_model_ready(libtorch_name, "3")) except Exception as ex: self.assertTrue(False, "unexpected error {}".format(ex)) - self._infer_success_models(["graphdef", "onnx"], (1, 3), model_shape) + self._infer_success_models(["openvino", "onnx"], (1, 3), model_shape) - # Remove the savedmodel from the first model repository and - # explicitly load savedmodel. The savedmodel from the second + # Remove the libtorch from the first model repository and + # explicitly load libtorch. The libtorch from the second # model repository should be loaded properly. In the second - # model repository savedmodel should have versions 1 and 3. - shutil.rmtree("models/" + savedmodel_name) + # model repository libtorch should have versions 1 and 3. + shutil.rmtree("models/" + libtorch_name) try: triton_client = httpclient.InferenceServerClient( "localhost:8000", verbose=True ) # Unload existing in-memory model from first model repository - triton_client.unload_model(savedmodel_name) + triton_client.unload_model(libtorch_name) # Load model from second model repository since original was deleted - triton_client.load_model(savedmodel_name) + triton_client.load_model(libtorch_name) except Exception as ex: - self.assertIn("failed to load '{}'".format(savedmodel_name), ex.message()) + self.assertIn("failed to load '{}'".format(libtorch_name), ex.message()) self._infer_success_models( - ["savedmodel", "graphdef", "onnx"], (1, 3), model_shape + ["libtorch", "openvino", "onnx"], (1, 3), model_shape ) def test_model_control(self): @@ -2038,7 +2038,7 @@ def test_multiple_model_repository_control_startup_models(self): plan_ensemble_name = ensemble_prefix + plan_name # Make sure unloaded models are not in the status - for base in ("savedmodel",): + for base in ("libtorch",): model_name = tu.get_model_name(base, np.float32, np.float32, np.float32) try: for triton_client in ( @@ -2257,13 +2257,12 @@ def test_model_repository_index(self): # use model control EXPLICIT and --load-model to load a subset of models # in model repository tensor_shape = (1, 16) - model_bases = ["graphdef", "savedmodel", "simple_savedmodel"] + model_bases = ["plan", "libtorch", "simple_libtorch"] # Sanity check on loaded models - # 3 models should be loaded: - # simple_savedmodel_float32_float32_float32 - # savedmodel_float32_float32_float32 - # graphdef_float32_float32_float32 + # 2 models should be loaded: + # simple_libtorch_float32_float32_float32 + # libtorch_float32_float32_float32 for model_base in model_bases: try: model_name = tu.get_model_name( @@ -2282,7 +2281,7 @@ def test_model_repository_index(self): # Check model repository index # All models should be in ready state except onnx_float32_float32_float32 # which appears in two repositories. - model_bases.append("simple_graphdef") + model_bases.append("simple_plan") try: triton_client = httpclient.InferenceServerClient( "localhost:8000", verbose=True @@ -3406,12 +3405,12 @@ def test_shutdown_with_live_connection(self): ) def test_add_custom_config(self): - models_base = ("savedmodel",) + models_base = ("libtorch",) models = list() for m in models_base: models.append(tu.get_model_name(m, np.float32, np.float32, np.float32)) - # Make sure savedmodel and plan are in the status + # Make sure libtorch and plan are in the status for model_name in models: try: for triton_client in ( @@ -3452,12 +3451,12 @@ def test_add_custom_config(self): self.assertTrue(False, "unexpected error {}".format(ex)) def test_delete_custom_config(self): - models_base = ("savedmodel",) + models_base = ("libtorch",) models = list() for m in models_base: models.append(tu.get_model_name(m, np.float32, np.float32, np.float32)) - # Make sure savedmodel and plan are in the status + # Make sure libtorch and plan are in the status for model_name in models: try: for triton_client in ( diff --git a/qa/L0_lifecycle/test.sh b/qa/L0_lifecycle/test.sh index f1a33886ac..bcd1713bb8 100755 --- a/qa/L0_lifecycle/test.sh +++ b/qa/L0_lifecycle/test.sh @@ -333,15 +333,18 @@ LOG_IDX=$((LOG_IDX+1)) # LifeCycleTest.test_parse_error_modelfail rm -fr models models_0 mkdir models models_0 -for i in graphdef savedmodel ; do +for i in openvino libtorch ; do cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models/. + if [ $i == "openvino" ]; then + echo 'parameters { key: "ENABLE_BATCH_PADDING" value { string_value: "YES" } }' >> models/openvino_float32_float32_float32/config.pbtxt + fi done for i in onnx plan ; do cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models_0/. done # Change the model files so that multiple versions will be loaded, and one of # the versions will fail to load and cause all other versions to be unloaded. -rm models/graphdef_float32_float32_float32/3/* +rm models/libtorch_float32_float32_float32/3/* SERVER_ARGS="--model-repository=`pwd`/models --model-repository=`pwd`/models_0 \ --exit-on-error=false --exit-timeout-secs=5" @@ -395,13 +398,13 @@ LOG_IDX=$((LOG_IDX+1)) # LifeCycleTest.test_parse_error_no_model_config rm -fr models models_0 mkdir models models_0 -for i in graphdef savedmodel ; do +for i in openvino libtorch ; do cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models/. done for i in onnx plan ; do cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models_0/. done -rm models/graphdef_float32_float32_float32/config.pbtxt +rm models/openvino_float32_float32_float32/config.pbtxt # Autocomplete should not be turned on for this test because it asserts an error was logged # when in strict model configuration mode. @@ -428,7 +431,7 @@ kill $SERVER_PID wait $SERVER_PID # check server log for the warning messages -if [ `grep -c "failed to open text file for read" $SERVER_LOG` == "0" ] || [ `grep -c "graphdef_float32_float32_float32/config.pbtxt: No such file or directory" $SERVER_LOG` == "0" ]; then +if [ `grep -c "failed to open text file for read" $SERVER_LOG` == "0" ] || [ `grep -c "openvino_float32_float32_float32/config.pbtxt: No such file or directory" $SERVER_LOG` == "0" ]; then echo -e "\n***\n*** Server log ${SERVER_LOG} did not print model load failure\n***" echo -e "\n***\n*** Test Failed\n***" RET=1 @@ -443,8 +446,11 @@ cp -r $DATADIR/qa_sequence_model_repository/onnx_sequence_int32 models/. cp -r $DATADIR/qa_model_repository/onnx_int32_int32_int32 models_0/. sed -i "s/OUTPUT/_OUTPUT/" models/onnx_sequence_int32/config.pbtxt sed -i "s/OUTPUT/_OUTPUT/" models_0/onnx_int32_int32_int32/config.pbtxt -for i in graphdef savedmodel; do +for i in openvino libtorch; do cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models/. + if [ $i == "openvino" ]; then + echo 'parameters { key: "ENABLE_BATCH_PADDING" value { string_value: "YES" } }' >> models/openvino_float32_float32_float32/config.pbtxt + fi done for i in onnx ; do cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models_0/. @@ -477,12 +483,12 @@ LOG_IDX=$((LOG_IDX+1)) # LifeCycleTest.test_parse_error_model_no_version rm -fr models mkdir models -for i in savedmodel onnx plan ; do +for i in libtorch onnx plan ; do cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models/. done -mkdir -p models/graphdef_float32_float32_float32 -cp $DATADIR/qa_model_repository/graphdef_float32_float32_float32/config.pbtxt \ - models/graphdef_float32_float32_float32/. +mkdir -p models/openvino_float32_float32_float32 +cp $DATADIR/qa_model_repository/openvino_float32_float32_float32/config.pbtxt \ + models/openvino_float32_float32_float32/. SERVER_ARGS="--model-repository=`pwd`/models --exit-on-error=false \ --exit-timeout-secs=5" @@ -511,7 +517,7 @@ LOG_IDX=$((LOG_IDX+1)) # LifeCycleTest.test_parse_ignore_zero_prefixed_version rm -fr models mkdir models -for i in savedmodel ; do +for i in libtorch ; do cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models/. mv models/${i}_float32_float32_float32/3 models/${i}_float32_float32_float32/003 done @@ -546,7 +552,7 @@ LOG_IDX=$((LOG_IDX+1)) # LifeCycleTest.test_parse_ignore_non_intergral_version rm -fr models mkdir models -for i in savedmodel ; do +for i in libtorch ; do cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models/. mv models/${i}_float32_float32_float32/3 models/${i}_float32_float32_float32/abc done @@ -579,12 +585,12 @@ fi LOG_IDX=$((LOG_IDX+1)) # LifeCycleTest.test_dynamic_model_load_unload -rm -fr models savedmodel_float32_float32_float32 +rm -fr models libtorch_float32_float32_float32 mkdir models -for i in graphdef onnx plan ; do +for i in openvino onnx plan ; do cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models/. done -cp -r $DATADIR/qa_model_repository/savedmodel_float32_float32_float32 . +cp -r $DATADIR/qa_model_repository/libtorch_float32_float32_float32 . SERVER_ARGS="--model-repository=`pwd`/models --repository-poll-secs=1 \ --model-control-mode=poll --exit-timeout-secs=5" @@ -608,12 +614,12 @@ wait $SERVER_PID LOG_IDX=$((LOG_IDX+1)) # LifeCycleTest.test_dynamic_model_load_unload_disabled -rm -fr models savedmodel_float32_float32_float32 +rm -fr models libtorch_float32_float32_float32 mkdir models -for i in graphdef onnx plan; do +for i in openvino onnx plan; do cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models/. done -cp -r $DATADIR/qa_model_repository/savedmodel_float32_float32_float32 . +cp -r $DATADIR/qa_model_repository/libtorch_float32_float32_float32 . SERVER_ARGS="--model-repository=`pwd`/models --model-control-mode=none \ --exit-timeout-secs=5" @@ -639,7 +645,7 @@ LOG_IDX=$((LOG_IDX+1)) # LifeCycleTest.test_dynamic_version_load_unload rm -fr models mkdir models -for i in graphdef ; do +for i in libtorch ; do cp -r $DATADIR/qa_model_repository/${i}_int32_int32_int32 models/. done @@ -667,7 +673,7 @@ LOG_IDX=$((LOG_IDX+1)) # LifeCycleTest.test_dynamic_version_load_unload_disabled rm -fr models mkdir models -for i in graphdef ; do +for i in libtorch ; do cp -r $DATADIR/qa_model_repository/${i}_int32_int32_int32 models/. done @@ -696,7 +702,7 @@ LOG_IDX=$((LOG_IDX+1)) # LifeCycleTest.test_dynamic_model_modify rm -fr models config.pbtxt.* mkdir models -for i in savedmodel plan ; do +for i in libtorch plan ; do cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models/. sed '/^version_policy/d' \ $DATADIR/qa_model_repository/${i}_float32_float32_float32/config.pbtxt > config.pbtxt.${i} @@ -731,7 +737,7 @@ LOG_IDX=$((LOG_IDX+1)) # LifeCycleTest.test_dynamic_file_delete rm -fr models config.pbtxt.* mkdir models -for i in savedmodel plan; do +for i in onnx plan; do cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models/. done @@ -757,17 +763,18 @@ wait $SERVER_PID LOG_IDX=$((LOG_IDX+1)) # LifeCycleTest.test_multiple_model_repository_polling -rm -fr models models_0 savedmodel_float32_float32_float32 +rm -fr models models_0 libtorch_float32_float32_float32 mkdir models models_0 -for i in graphdef ; do +for i in openvino ; do cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models/. + echo 'parameters { key: "ENABLE_BATCH_PADDING" value { string_value: "YES" } }' >> models/openvino_float32_float32_float32/config.pbtxt done for i in onnx ; do cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models_0/. done -cp -r $DATADIR/qa_model_repository/savedmodel_float32_float32_float32 . -cp -r $DATADIR/qa_model_repository/savedmodel_float32_float32_float32 models/. && \ - rm -rf models/savedmodel_float32_float32_float32/3 +cp -r $DATADIR/qa_model_repository/libtorch_float32_float32_float32 . +cp -r $DATADIR/qa_model_repository/libtorch_float32_float32_float32 models/. && \ + rm -rf models/libtorch_float32_float32_float32/3 SERVER_ARGS="--model-repository=`pwd`/models --model-repository=`pwd`/models_0 \ --model-control-mode=poll --repository-poll-secs=1 --exit-timeout-secs=5" @@ -791,17 +798,18 @@ wait $SERVER_PID LOG_IDX=$((LOG_IDX+1)) # LifeCycleTest.test_multiple_model_repository_control -rm -fr models models_0 savedmodel_float32_float32_float32 +rm -fr models models_0 libtorch_float32_float32_float32 mkdir models models_0 -for i in graphdef ; do +for i in openvino ; do cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models/. + echo 'parameters { key: "ENABLE_BATCH_PADDING" value { string_value: "YES" } }' >> models/openvino_float32_float32_float32/config.pbtxt done for i in onnx ; do cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models_0/. done -cp -r $DATADIR/qa_model_repository/savedmodel_float32_float32_float32 . -cp -r $DATADIR/qa_model_repository/savedmodel_float32_float32_float32 models/. && \ - rm -rf models/savedmodel_float32_float32_float32/3 +cp -r $DATADIR/qa_model_repository/libtorch_float32_float32_float32 . +cp -r $DATADIR/qa_model_repository/libtorch_float32_float32_float32 models/. && \ + rm -rf models/libtorch_float32_float32_float32/3 # Show model control mode will override deprecated model control options SERVER_ARGS="--model-repository=`pwd`/models --model-repository=`pwd`/models_0 \ @@ -933,8 +941,8 @@ for i in plan onnx ; do sed -i "s/max_batch_size:.*/max_batch_size: 1/" models_0/simple_${i}_float32_float32_float32/config.pbtxt done -# savedmodel doesn't load because it is duplicated in 2 repositories -for i in savedmodel ; do +# libtorch doesn't load because it is duplicated in 2 repositories +for i in libtorch ; do cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models/. cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models_0/. done @@ -943,7 +951,7 @@ SERVER_ARGS="--model-repository=`pwd`/models --model-repository=`pwd`/models_0 \ --model-control-mode=explicit \ --strict-readiness=false \ --strict-model-config=false --exit-on-error=false \ - --load-model=savedmodel_float32_float32_float32 \ + --load-model=libtorch_float32_float32_float32 \ --load-model=plan_float32_float32_float32 \ --load-model=simple_onnx_float32_float32_float32" SERVER_LOG="./inference_server_$LOG_IDX.log" @@ -978,8 +986,8 @@ for i in plan onnx ; do sed -i "s/max_batch_size:.*/max_batch_size: 1/" models_0/simple_${i}_float32_float32_float32/config.pbtxt done -# savedmodel doesn't load because it is duplicated in 2 repositories -for i in savedmodel ; do +# libtorch doesn't load because it is duplicated in 2 repositories +for i in libtorch ; do cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models/. cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models_0/. done @@ -1070,7 +1078,7 @@ LOG_IDX=$((LOG_IDX+1)) rm -fr models models_0 config.pbtxt.* mkdir models models_0 # Ensemble models in the second repository -for i in graphdef savedmodel ; do +for i in plan libtorch ; do cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models/. cp -r $DATADIR/qa_ensemble_model_repository/qa_model_repository/simple_${i}_float32_float32_float32 models_0/. done @@ -1086,8 +1094,8 @@ SERVER_ARGS="--model-repository=`pwd`/models --model-repository=`pwd`/models_0 \ --strict-readiness=false \ --strict-model-config=false --exit-on-error=false \ --load-model=onnx_float32_float32_float32 \ - --load-model=graphdef_float32_float32_float32 \ - --load-model=simple_savedmodel_float32_float32_float32" + --load-model=plan_float32_float32_float32 \ + --load-model=simple_libtorch_float32_float32_float32" SERVER_LOG="./inference_server_$LOG_IDX.log" run_server if [ "$SERVER_PID" == "0" ]; then @@ -1369,7 +1377,7 @@ done # Send HTTP request to control endpoint rm -fr models config.pbtxt.* mkdir models -for i in graphdef savedmodel onnx plan ; do +for i in openvino libtorch onnx plan ; do cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models/. done @@ -1388,7 +1396,7 @@ fi # unload API should return bad request set +e -code=`curl -s -w %{http_code} -o ./curl.out -X POST localhost:8000/v2/repository/models/graphdef_float32_float32_float32/unload` +code=`curl -s -w %{http_code} -o ./curl.out -X POST localhost:8000/v2/repository/models/openvino_float32_float32_float32/unload` set -e if [ "$code" == "200" ]; then echo -e "\n***\n*** Test Failed\n***" @@ -1397,7 +1405,7 @@ fi # the model should be available/ready set +e -code=`curl -s -w %{http_code} localhost:8000/v2/models/graphdef_float32_float32_float32/ready` +code=`curl -s -w %{http_code} localhost:8000/v2/models/openvino_float32_float32_float32/ready` set -e if [ "$code" != "200" ]; then echo -e "\n***\n*** Test Failed\n***" @@ -1405,11 +1413,11 @@ if [ "$code" != "200" ]; then fi # remove model file so that if reload is triggered, model will become unavailable -rm models/graphdef_float32_float32_float32/*/* +rm models/openvino_float32_float32_float32/*/* # load API should return bad request set +e -code=`curl -s -w %{http_code} -o ./curl.out -X POST localhost:8000/v2/repository/models/graphdef_float32_float32_float32/load` +code=`curl -s -w %{http_code} -o ./curl.out -X POST localhost:8000/v2/repository/models/openvino_float32_float32_float32/load` set -e if [ "$code" == "200" ]; then echo -e "\n***\n*** Test Failed\n***" @@ -1418,7 +1426,7 @@ fi # the model should be available/ready set +e -code=`curl -s -w %{http_code} localhost:8000/v2/models/graphdef_float32_float32_float32/ready` +code=`curl -s -w %{http_code} localhost:8000/v2/models/openvino_float32_float32_float32/ready` set -e if [ "$code" != "200" ]; then echo -e "\n***\n*** Test Failed\n***" @@ -1434,7 +1442,7 @@ LOG_IDX=$((LOG_IDX+1)) # some more comprehensive fuzz attacks. rm -fr models mkdir models -for i in graphdef ; do +for i in openvino ; do cp -r $DATADIR/qa_model_repository/${i}_int32_int32_int32 models/. done @@ -2136,7 +2144,7 @@ LOG_IDX=$((LOG_IDX+1)) # LifeCycleTest.test_add_custom_config rm -fr models config.pbtxt.* mkdir models -for i in savedmodel; do +for i in libtorch; do cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models/. mkdir models/${i}_float32_float32_float32/configs sed 's/^version_policy:.*/version_policy: { specific: { versions: [2] }}/' \ @@ -2168,7 +2176,7 @@ LOG_IDX=$((LOG_IDX+1)) # LifeCycleTest.test_delete_custom_config rm -fr models config.pbtxt.* mkdir models -for i in savedmodel; do +for i in libtorch; do cp -r $DATADIR/qa_model_repository/${i}_float32_float32_float32 models/. mkdir models/${i}_float32_float32_float32/configs sed 's/^version_policy:.*/version_policy: { specific: { versions: [2] }}/' \ diff --git a/qa/L0_long_running_stress/scenarios.py b/qa/L0_long_running_stress/scenarios.py index abb0004e90..650f228ab5 100755 --- a/qa/L0_long_running_stress/scenarios.py +++ b/qa/L0_long_running_stress/scenarios.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2021-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -84,10 +84,8 @@ def get_trial(self): def get_datatype(self, trial): # Get the datatype to use based on what models are available (see test.sh) - if ("plan" in trial) or ("savedmodel" in trial): + if "plan" in trial: return np.float32 - if "graphdef" in trial: - return np.dtype(object) return np.int32 # FIXME do we need client meta data? @@ -205,7 +203,7 @@ def __init__( # Add no validation models self.options_.append( PerfAnalyzerScenario.ModelOption( - "resnet_v1_50_graphdef_def", 32, (1, 4, 1), queue_latency_range_us + "resnet_v1_50_def", 32, (1, 4, 1), queue_latency_range_us ) ) for trial in sequence_trials: @@ -280,34 +278,19 @@ def generate_sequence_data(self, trial, dtype, data_filename): input_data.append({input0: [res]}) output0 = "OUTPUT" if "libtorch" not in trial else "OUTPUT__0" output_data = [] - if ("savedmodel" in trial) and ("nobatch" in trial): - # Special case where the model is accumulator - sum = 0 - for i in range(3): - sum += i - if dtype == np.float32: - res = float(sum) - elif dtype == np.int32: - res = sum - elif dtype == np.dtype(object): - res = str(sum) - else: - raise Exception("unexpected sequence data type {}".format(dtype)) - output_data.append({output0: [res]}) - else: - for i in range(3): - res = 1 if i == 0 else i - if dtype == np.float32: - res = float(res) - elif dtype == np.int32: - res = int(res) - elif dtype == np.dtype(object): - res = str(res) - else: - raise Exception("unexpected sequence data type {}".format(dtype)) - output_data.append( - {output0: [res if dtype != np.dtype(object) else str(res)]} - ) + for i in range(3): + res = 1 if i == 0 else i + if dtype == np.float32: + res = float(res) + elif dtype == np.int32: + res = int(res) + elif dtype == np.dtype(object): + res = str(res) + else: + raise Exception("unexpected sequence data type {}".format(dtype)) + output_data.append( + {output0: [res if dtype != np.dtype(object) else str(res)]} + ) data = {"data": [input_data]} data["validation_data"] = [output_data] @@ -351,7 +334,7 @@ def run(self, client_metadata): class ResNetScenario(Scenario): def __init__(self, name, batch_size=32, verbose=False, out_stream=sys.stdout): super().__init__(name, [], verbose, out_stream) - self.model_name_ = "resnet_v1_50_graphdef_def" + self.model_name_ = "resnet_v1_50_def" self.batch_size_ = batch_size img = self.preprocess("../images/vulture.jpeg") @@ -525,7 +508,6 @@ def get_expected_result(self, expected_result, value, trial, flag_str=None): # information. if ( ("nobatch" not in trial and ("custom" not in trial)) - or ("graphdef" in trial) or ("plan" in trial) or ("onnx" in trial) ) or ("libtorch" in trial): @@ -555,9 +537,7 @@ def check_sequence_async( """ if ( - ("savedmodel" not in trial) - and ("graphdef" not in trial) - and ("custom" not in trial) + ("custom" not in trial) and ("onnx" not in trial) and ("libtorch" not in trial) and ("plan" not in trial) diff --git a/qa/L0_long_running_stress/stress.py b/qa/L0_long_running_stress/stress.py index 978f204ee6..ae67d27668 100755 --- a/qa/L0_long_running_stress/stress.py +++ b/qa/L0_long_running_stress/stress.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2021-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -47,7 +47,7 @@ FLAGS = None CORRELATION_ID_BLOCK_SIZE = 1024 * 1024 -BACKENDS = os.environ.get("BACKENDS", "graphdef savedmodel onnx plan") +BACKENDS = os.environ.get("BACKENDS", "onnx plan") _thread_exceptions = [] _thread_exceptions_mutex = threading.Lock() @@ -66,7 +66,7 @@ def get_trials(is_sequence=True): _trials = () if is_sequence: for backend in BACKENDS.split(" "): - if (backend != "libtorch") and (backend != "savedmodel"): + if backend != "libtorch": _trials += (backend + "_nobatch",) _trials += (backend,) else: diff --git a/qa/L0_long_running_stress/test.sh b/qa/L0_long_running_stress/test.sh index b98a89f955..1973028bcf 100755 --- a/qa/L0_long_running_stress/test.sh +++ b/qa/L0_long_running_stress/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2021, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -63,7 +63,7 @@ fi RET=0 # If BACKENDS not specified, set to all -BACKENDS=${BACKENDS:="graphdef savedmodel onnx libtorch"} +BACKENDS=${BACKENDS:="onnx libtorch"} export BACKENDS export CI_JOB_ID=${CI_JOB_ID} @@ -75,10 +75,8 @@ rm -fr *.log *.txt models validation_data csv_dir && mkdir models validation_da # Get the datatype to use based on the backend function get_datatype () { local dtype='int32' - if [[ $1 == "plan" ]] || [[ $1 == "savedmodel" ]]; then + if [[ $1 == "plan" ]]; then dtype='float32' - elif [[ $1 == "graphdef" ]]; then - dtype='object' fi echo $dtype } @@ -132,9 +130,9 @@ cp -r ../custom_models/custom_zero_1_float32 $MODEL_DIR/custom_zero_1_float32 && echo "{ key: \"execute_delay_ms\"; value: { string_value: \"10000\" }}" >> config.pbtxt && \ echo "]" >> config.pbtxt) -cp -r $DATADIR/tf_model_store/resnet_v1_50_graphdef $MODEL_DIR/resnet_v1_50_graphdef_def && \ - (cd $MODEL_DIR/resnet_v1_50_graphdef_def && \ - sed -i 's/^name: "resnet_v1_50_graphdef"/name: "resnet_v1_50_graphdef_def"/' config.pbtxt && \ +cp -r $DATADIR/onnx_model_store/resnet_v1_50 $MODEL_DIR/resnet_v1_50_def && \ + (cd $MODEL_DIR/resnet_v1_50_def && \ + sed -i 's/^name: "resnet_v1_50"/name: "resnet_v1_50_def"/' config.pbtxt && \ echo "optimization { }" >> config.pbtxt) SERVER_ARGS="--model-repository=`pwd`/$MODEL_DIR" diff --git a/qa/L0_model_config/autofill_noplatform/tensorflow_savedmodel/bad_input_dims/1/model.savedmodel/saved_model.pb b/qa/L0_model_config/autofill_noplatform/tensorflow_savedmodel/bad_input_dims/1/model.savedmodel/saved_model.pb deleted file mode 100644 index a76abafbf76990f47d12e0697e119113c0940adb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1407 zcmb_b&2G~`5Uw5DaVKq(PC-IjPCgK1iPX-~w~}z^C9SY?;SweGmPS~1WUndVL3kUU zg)7g1kYLw;)FGf0;mhpo`kU{YKV8Ca0AFPMEQ15Biy%M^qz{JV3A^EzaQl&4;|%zv z!ZvH_^r1UC>YhrnqN%Mz9osMkWxPlk9tyDHCca1babqZxlzGMx!JmMP(VsA(5XSz!3DyfJSV^HVB}uqIJfEm=0)h#tO&a3}qA;L+3hN`1Cdo1DcRt z{hJyH#l|rdhniGPZx?Hdg&~R~KaoTM+-&*40-WR(Fw~SL@2Ls)&>jt~7l}U_!E%sA z@1poF8sH}l)^O~-nz~o7=aC8+GLK!a+l+RT@vqo_(*tk#2u&s^=7==2ZMwGE7QK5&t|GQOdx@e&&0tp3wf8Sy zB?5d<#}_W|Nj}9yBw<21v_fR}-jMR~6mR(m%a*n`TSa15Bs`n{PvSzioU*H#mycP! zNTkSdZ^2cMG}sPm<5tkRpZk{s4-8p9GrvpF6RWd|-p&Jhv&ce*UnQ_WE4Sns^crj9 zSp63HeHChijavx&4+tDVyQYhrnqN%Mz9osMkWxPlk9tyDHCca1babqZxlzGMx!JmMP(VsA(5XSz!3DyfJSV^HVB}uqIJfEm=0)h#tO&a3}qA;L+3hN`1Cdo1DcRt z{hJyH#l|rdhniGPZx?Hdg&~R~KaoTM+-&*40-WR(Fw~SL@2Ls)&>jt~7l}U_!E%sA z@1poF8sH}l)^O~-nz~o7=aC8+GLK!a+l+RT@vqo_(*tk#2u&s^=7==2ZMwGE7QK5&t|GQOdx@e&&0tp3wf8Sy zB?5d<#}_W|Nj}9yBw<21v_fR}-jMR~6mR(m%a*n`TSa15Bs`n{PvSzioU*H#mycP! zNTkSdZ^2cMG}sPm<5tkRpZk{s4-8p9GrvpF6RWd|-p&Jhv&ce*UnQ_WE4Sns^crj9 zSp63HeHChijavx&4+tDVyQYhrnqN%Mz9osMkWxPlk9tyDHCca1babqZxlzGMx!JmMP(VsA(5XSz!3DyfJSV^HVB}uqIJfEm=0)h#tO&a3}qA;L+3hN`1Cdo1DcRt z{hJyH#l|rdhniGPZx?Hdg&~R~KaoTM+-&*40-WR(Fw~SL@2Ls)&>jt~7l}U_!E%sA z@1poF8sH}l)^O~-nz~o7=aC8+GLK!a+l+RT@vqo_(*tk#2u&s^=7==2ZMwGE7QK5&t|GQOdx@e&&0tp3wf8Sy zB?5d<#}_W|Nj}9yBw<21v_fR}-jMR~6mR(m%a*n`TSa15Bs`n{PvSzioU*H#mycP! zNTkSdZ^2cMG}sPm<5tkRpZk{s4-8p9GrvpF6RWd|-p&Jhv&ce*UnQ_WE4Sns^crj9 zSp63HeHChijavx&4+tDVyQ 0 followed by the explicit tensor shape, making complete shape \[-1,1\]) \ No newline at end of file diff --git a/qa/L0_model_config/autofill_noplatform/tensorflow_savedmodel/bad_output_type/1/model.savedmodel/saved_model.pb b/qa/L0_model_config/autofill_noplatform/tensorflow_savedmodel/bad_output_type/1/model.savedmodel/saved_model.pb deleted file mode 100644 index a76abafbf76990f47d12e0697e119113c0940adb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1407 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 KqEFCBL4zB?OKKMY diff --git a/qa/L0_model_config/autofill_noplatform_success/tensorflow_graphdef/no_name_platform/config.pbtxt b/qa/L0_model_config/autofill_noplatform_success/tensorflow_graphdef/no_name_platform/config.pbtxt deleted file mode 100644 index 49b49aadbb..0000000000 --- a/qa/L0_model_config/autofill_noplatform_success/tensorflow_graphdef/no_name_platform/config.pbtxt +++ /dev/null @@ -1,25 +0,0 @@ -max_batch_size: 1 -input [ - { - name: "INPUT0" - data_type: TYPE_INT32 - dims: [ 16 ] - }, - { - name: "INPUT1" - data_type: TYPE_INT32 - dims: [ 16 ] - } -] -output [ - { - name: "OUTPUT0" - data_type: TYPE_INT8 - dims: [ 16 ] - }, - { - name: "OUTPUT1" - data_type: TYPE_INT8 - dims: [ 16 ] - } -] diff --git a/qa/L0_model_config/autofill_noplatform_success/tensorflow_graphdef/no_name_platform/expected b/qa/L0_model_config/autofill_noplatform_success/tensorflow_graphdef/no_name_platform/expected deleted file mode 100644 index ed01acd5e0..0000000000 --- a/qa/L0_model_config/autofill_noplatform_success/tensorflow_graphdef/no_name_platform/expected +++ /dev/null @@ -1,45 +0,0 @@ -name: "no_name_platform" -platform: "tensorflow_graphdef" -version_policy { - latest { - num_versions: 1 - } -} -max_batch_size: 1 -input { - name: "INPUT0" - data_type: TYPE_INT32 - dims: 16 -} -input { - name: "INPUT1" - data_type: TYPE_INT32 - dims: 16 -} -output { - name: "OUTPUT0" - data_type: TYPE_INT8 - dims: 16 -} -output { - name: "OUTPUT1" - data_type: TYPE_INT8 - dims: 16 -} -instance_group { - name: "no_name_platform" - count: 1 - gpus: 0 - kind: KIND_GPU -} -default_model_filename: "model.graphdef" -optimization { - input_pinned_memory { - enable: true - } - output_pinned_memory { - enable: true - } -} -backend: "tensorflow" -runtime: "" diff --git a/qa/L0_model_config/autofill_noplatform_success/tensorflow_graphdef/reshape_config_provided/config.pbtxt b/qa/L0_model_config/autofill_noplatform_success/tensorflow_graphdef/reshape_config_provided/config.pbtxt deleted file mode 100644 index b3bc21377e..0000000000 --- a/qa/L0_model_config/autofill_noplatform_success/tensorflow_graphdef/reshape_config_provided/config.pbtxt +++ /dev/null @@ -1,41 +0,0 @@ -name: "reshape_config_provided" -version_policy { - latest { - num_versions: 1 - } -} -max_batch_size: 8 -input { - name: "INPUT0" - data_type: TYPE_FP32 - dims: 1 - reshape { - } -} -input { - name: "INPUT1" - data_type: TYPE_FP32 - dims: 8 - reshape { - shape: 4 - shape: 1 - shape: 2 - } -} -output { - name: "OUTPUT0" - data_type: TYPE_FP32 - dims: 1 - reshape { - } -} -output { - name: "OUTPUT1" - data_type: TYPE_FP32 - dims: 8 - reshape { - shape: 4 - shape: 1 - shape: 2 - } -} diff --git a/qa/L0_model_config/autofill_noplatform_success/tensorflow_graphdef/reshape_config_provided/expected b/qa/L0_model_config/autofill_noplatform_success/tensorflow_graphdef/reshape_config_provided/expected deleted file mode 100644 index 51e2d46d42..0000000000 --- a/qa/L0_model_config/autofill_noplatform_success/tensorflow_graphdef/reshape_config_provided/expected +++ /dev/null @@ -1,59 +0,0 @@ -name: "reshape_config_provided" -platform: "tensorflow_graphdef" -version_policy { - latest { - num_versions: 1 - } -} -max_batch_size: 8 -input { - name: "INPUT0" - data_type: TYPE_FP32 - dims: 1 - reshape { - } -} -input { - name: "INPUT1" - data_type: TYPE_FP32 - dims: 8 - reshape { - shape: 4 - shape: 1 - shape: 2 - } -} -output { - name: "OUTPUT0" - data_type: TYPE_FP32 - dims: 1 - reshape { - } -} -output { - name: "OUTPUT1" - data_type: TYPE_FP32 - dims: 8 - reshape { - shape: 4 - shape: 1 - shape: 2 - } -} -instance_group { - name: "reshape_config_provided" - count: 1 - gpus: 0 - kind: KIND_GPU -} -default_model_filename: "model.graphdef" -optimization { - input_pinned_memory { - enable: true - } - output_pinned_memory { - enable: true - } -} -backend: "tensorflow" -runtime: "" diff --git a/qa/L0_model_config/autofill_noplatform_success/tensorflow_savedmodel/cpu_instance/1/model.savedmodel/saved_model.pb b/qa/L0_model_config/autofill_noplatform_success/tensorflow_savedmodel/cpu_instance/1/model.savedmodel/saved_model.pb deleted file mode 100644 index a76abafbf76990f47d12e0697e119113c0940adb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1407 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�E%m+Uxw5`#7xI95b^zyKp zT)Q2SVGh_uNb!~GaYvT^<*53tnapEp_0%4tZyegNa7A6g29(#zB!P!AZ zmUTCle5Lc(ck<50+W8o{tBC(*{m&PzAF|F_jWw_mhm|?Ly~y(IHg)UbNGad+*>s6dWCAGu{f;0{ogP4mrk*xQ zcYW_HFb&?NYB=r6$nP5kdrD^FhN za@-(y(25J1_$wK6KezedWU_&RFE`6#P3awxqbb}lQ@tU$povePx1%h!B_3$v&x*=- z7eNz$Eu(~W4rkvHkc+EttbeTYrtu6fr;`K|r|MW^`Q*^zbE;R$woDfTNy@=Ly&F6>>y&^z zy?c}TZeH1C2}A^g$GNNLGh%7;Pwxh^I1;83h(r#$3lg{79Z(VTD)0}VqTG5ruQb2Z ztfh0{cG@UEu=>TU(@x=fTw$sjy=6kBc78O)qc-ZCioP9r*rapv@bHt`4f5bA)@z6Q@C6y(536F^1X8kO54y*!8hzij;JMy?5QA+=Ae%javcZ!EW zBE+d0z*XytX)8V!qIL0#k~pV+C;X7x43S4Hj_+nqh_|n>ULNz;H2j;{0-}iE?wKTW zf(8$q&M;O+lpa<#U?{j|-`~O~+Civqn6Ykw_yQTdxVct^UqsudFQJ(~CXwczKisFj zeuZ^Y$6r;bCgut-*{y*Vxz>e;H`n|7od-Kk8QovS$QxEGsJyD!w~3h8r$ zxJPpGi%js_81nM(-k8-l^EySH@SGlgseMeKUsR@#di(|i#4!dl6+{=3?73)}@idrI;LhlJ9{JXijkKxHFx_Xud!VFCDpP8TRp!FrJ%hV(9 z13;orE=<~0es=C#XRIC_aOQN3sC>U7DTtth)4^~-bh{6ZED z2hA3b8~$e9y$EGLWK6yib!}6du2e7;NA`Yo&f4x_oQO{DwXn-N9yAt6nHj%*u8x%I zs6nXel)P)|MjRK&lYB`SOX>FIrA}k4B|Gqqi2*bs_7Ty}GsrlpJ-WB;uk`jT4LH6^&KHrbL!|dhG=aIj$%oPMjt~fq1nsmOFr*e z#yXA;Z433V4UVBbBHGUu}Yo_Q*s%?=3mnZvhrO+*rF9o zo01C=ws>`W31RbBcWDu}Xe9**n?J@7kYos(|Gmcz=j-YawqNJ9`yAs*aJAaDFIZe< z|JmiSwSl0$gsu53Msp}?mn!hj0q2IDs-(2#FA=0QWxLh)$*{Fx{;=(Zq=w`V<8(#< ztl2O}$4^n^-(OG#u%?qLN%7vFH3HV^`B|!#Jp&}D8bcb+Pf2!OLR2l_rztQLoY{!MDn!hBwIdIxgEYbe1SfdzLvo#l@YCb#Q2C@WI^T#v0SrMv+ z3?N6<{6VPD=xx_g)U%Hxh+2-#zMcAF2+oaY{ za0TRJeM6p`C9s?w4`p*Q4|XO<=Fv*O>sG?p(W$w_NC3-OiiEJ7`)>J0w<9i0k0l$Y zj~DubWimJ5vWRRst&zq8k);TgZG$6ax@Wr}W3|h%AlJ?^}3pm5yqE+`EG~d7AY-D-tl*U)u0* zW(x*R!)=QE?mGQnlE;vOh71C1+KQ3VU+UVs3$znn!jCsV|7s?=&f*Je3?Au(-Lz&{ z{w;6M@LemC>q!3MhJPDPGS{w4yEGgeyEF)@xBg;APIaF$Y7}4p)=#)_np~Y2u7CT? zAu@t{1ZOY&;f8-h!FP8z8O}6v`0^DcxYghyG)7IgxLb{<;oOgPi zJ#q1XdimmI!V?$S6^l}fx2>p`xn19hj7#_DrsNQ07Xq}!AA9(Bq=M~5v1vu|x#|^* z`;-S<^#Zm$bL>j3qCtnm9lAPbB~^lY}VHxRUg zKvs(7rhCb1-@3x@3W1Op3nDPue-=gbcHUFelaD-c`8KEN)?)rvf)i{9*MeJ8!7gHV zPOxDs(4yZXJtEN6?U@s7V0GS^EyKUv>JfphM0$b^j4|Zbf)i}udyg9~=XCg~9Y zR^wmqUwoSX|I3|YvjgSNvGsn{5i6NskCHYjl*Y2}?_rQ+S$RmYimT zEH#iDyB-1u5T0fOpQCQ18lsc*hyV*3OSTTiVORdaKuf5uRJD4)=X4>2>sp*{tXjgT3Mae0GyA)ieM}*eu)8`)CT06hJb+Tjkh+x`+ zleI*}B|RcMnzd|9MqlE_dY18fM0gXUYTn>dilj$`w;UUMovN4cqi1K=z^&J$?vitQ z9t&WSBkmF5jWx1Ua857&Sf}ff9ueLER66OU>LnsFMDfq*MQg%vx|(x(-UQ?7k|)R{ zJtDlJDSig&xnRfpP}G@4a4s)e7sk_?b9vqb)+>loav8$r-}wl#@?AsNq7_V=l1q9- z*t~B56NcP(FClC`?>tP&?5uVXVT)E$fUx;vjJ)P|VF$AExZyIII)qKqBLb|(Uh)_8 zh_FNKC2Y+X5t>6$yHtUH4sTm8vv3i%<}b0yZdKAF!U}El7$LSMsUi7fSoPs|CGf%s z9X~~te}6$0z?!5-1jB&b?2fE-ox zKSv#i<3eGrL5QmP3mOwHw+R#E>7myWqH6vkdY|REFbS#_%;)yt5>zc1q6Ag*2S7EU z!FF7Ts`*Q@$+y#nQfD!2%O&uPzYq|^YPRM=RLx(-;I`amN8BR<`0|mxGE~i9kd=?9 z^Ey}s<&3?pda2k9fg)7RClGSnxr9no&7WjkWh+$8ADZHAux&4m;8yxFR4rbg(`d)v zN>t6C#I94cg~xhC+#>A}p;g`Qut1B0-y?!)J?=2uY_*b?J_U}K`dO`e7xSmgMNF0S zh!|?+OFVt8cfV#seW`myz;(G&Ni~QngDUN%Lo{+zn?ONL+fLB)UsBT!zjV4-231OW zMEKG%*j>~|;1&riXK6v%>^xRssx0Xd;So0)qDzbfu$-kxCq?|$?cU_c^jNZSlJto1 z=XdEcB3pKk2yaAGwhfMyNqR(h%du|O$+yUE`=exn4V2T2Wy#0cElhY*(5no4vNOU| zs{nhlV~y++VNdX5jXguhp2dqGg0DU_r~1j0y{7UKzfU5|)&3VK9@iwjl8 zTzV#ZM68{_@0YKyojm)-Gn@LqK797{#hp`U&q%vPgv;ucPkSDA9{`R*Jy~6K^3>_A zYi`%%J zH-ZwN-PtXNl^4*ykkLee3Qazl&T29V&a{rmsbnarb!gE2^CseE;F11k z;pf{p&8HAG3CyuiyUdoS{J7nB&KU2IveuGIn`d52HF;~bhU=X42;SGt7%kv({Pd2W z841@M#{2#`quCj2P2;h=e8kI*Hf$Q!yL=Lo8_DC9^Obc+^*%XcbY9A>%kO9a+G1?* zBg1I&`Vims{Eu|9;aDMY^pDQfD@z=G;F!>YG==HpoABlMJk&l~X{5b=T!#02a<1;G zX5i2P!X8u0cf-iB$XCT~s=TPH7iGn{z`vQR7dK+Oj$`J8U}M%d;?QIwEOQ#)H&=Jm zv)&+{t<0yJ$dM&)Dmn&|kg9qI=i&F9MUQ(ihp{!0Km!x*y|H5h@A%=nTI47FF}dN! zYdcTSy}DYn(Qw!RpZ%V>%EQG>J<=}k3Io#BJE`HoV)KX$kz7e)qc7FoUoA;ksv7GL zFD}~h2Pu=`{Y#q-r7_^Qtd#BAgOyRmDz6GsFK9#l%j?O9GIFlywYCIyzq)U653dBq zv>);SN9lTJ}XP4!sSd`hS8z=B_L(L4JLqs-4Ndu4RT!FNQR)ny1|as%$i6wn}aRl3NJ%*23+SRhDRPP{*g`Ny`Ry z==c)u2~y|rzII;epS9g+ci1;YImB0QuRY3i_U8B@iCEuJ%MovvR43G)wcP{lV?l$h z(YF=uC!q=X+oC#d4O^n7p{Qm%^!tScZp$E;W{+E;JBt!{JF3|SFwPRBqUAV@-U9vg zTzzhphBCH4bwV{H-&@ByC=cgdUS-qmd(=x8FD_2Ep^%aXIlpS941TwhP<#)zJVliX zJB8JfZZh6~p5^_0>Yl|*oAF+_%_)YP{i|rfR_7AH&Cyz>-RBg;&G9<-Cfw|=(}Pei zDcl^bsS0lPN9(nBvlMRjNAh^(oNOK3tZ&+sRd4)t*(M);j~EHiJPfwifo7j%Q7NG) zScF{}`d%_+)lPregDzGA&7UX*nuC>u{bd>|VHreeV>2E83n40>>I#%u=aXgFl0Sd+ zz(Y&E*QIy4LJz7-N%aca`%lNCu$)A_8&M1~`z-N_C+*xoA;j#j3)P|q{cnmPW`A*ZpXD^E zfe!T7gPX_Vnwm;6#OyN*?&IqqW`AT;Xefl3k)diJW`8&;*jGc${v?L{LTJ+;3d+d2 zqGk}YKi#IF~@7hZb~$RnEmPO+C>oaUm*8}4vzPRraT$Y zl@0C_OzVWx_-34L*s15=6r%d}vU*VJ!mj|=-DP-tAME9N!mLb!2WKq#o&zY*2OA&?11)t%U9iv~jQuX0>CZ+R$KopnJy_W%71mHl#X@Xu!p` zz5#9tQs)&)VO7fzm|9lBs&>?o&73RR2&@W+(P{seuav>6b|@;f@KXMwYJ;7?kX#1s z*YQi zmK-iPF7I3Ph`u^&{v6Q%UVXgUcXH>==e8H~8&(pF+^0TRXsZ4qhkr9W7`0(ZcO(C} zR?yX`=(vX!JY^45ry=JLqP|D-`Fy57d;H0*ovqE+pFRK3*3ROJ#lCy*c+snluRcLt zaZXRF8v`sJx48Tj$L_gv^?G&n=Gluo=PvGSURY)&7Z#tXo_gdn7V|4-iK)a8`0DrQO4)@{f0?2BnT8`|+oR7SHD@X2 z^{%M;@2_WMTQ`xa>E;>_XtSQJR;v`9z|OT=nLh{gzmHN1uHH6RFROKO&~uMj&8v!z zJo%EqHKTiF8%Y2IT%AX@-i|Lhge>GR29G&-j&>nPpo%$Dx z*AyRg%2FY9le*d3Yuc#qiKr1wkR%mBfi$_s%XL#q?Ua?;ruzKFYbTJJAtRC18aK53 zyLUzE=a7==`dBjcocH#d&TXAphS0FLj+WDjtsTE%5GJ`Y(9M?@WysDb?y~HCzIxu` z7QkM{@?UZK)QPc^?a*#Xm?OMK-Nw-UJ}UbBauxN_QO6CI*420M_Wse}u`oQ(7uCVm zJI1@~>wWveb)19=#37Wzck}LhGdLAEoKHz!y-6KrXl!snwa>=Psm1bhsoywLHx*5t z%b%A>!6V+E1Nz_m{JDHtj8muO%X(y{@||ke!xtpHgOf&FH9d{~VJ6p=ehK55(n4vk zGg|c0ggb8(pBNRom7kxfe4}Wv62A`&Xi4<6-Z5j%>PBnR2Sd|4D|?`ECQZ`5YR)R8 z1ZI!cv?;);%&ByRDqPpH0?)u zXA}ZKz%tLc3%r?AKMDpKx!4^k{Zd1XKhE1d1@lr{b8n|97K@?fn3xvGPG`X{u?2#o z>bAwLpg@pI$cFvyxGwT^;BWG}M6vi6>_5wGit}hAh|@F=@3uS?G`0q#TWgmlu3w}M zGCYgD)e>w~@iSWoxLd=SdIv9RhqF(+Th+nln1mssBwo(yjuV$5e~fvSCGYyZw>1d| z?lFyBI7itTNwmq*#;3hsyulR^57(gF3`Gnv!U8mgT}-$Lc-q-J>?Q<^EVBTks}=#LC!4c_ zj9lw(Ecr_3vhU=bpSANb@>r1w%=(`%T0dl+v&$FS-0N0#WN|Y$f3@_D>q(qnI-q`$ zhcnqRDV_znYniQZ-eg=~11oW0n&aDxEZ=TZw=Ry9@=c##7kWk}yy9O133Ary;gx9W zX%kj1yb>>KPrxhwvMv$45-mXjulOJLil&cev2Z2-LyvH0V%6}K;Uut@A|;1-TPAysfC5*E4Q9pNX1_P+(U^V6@MVJh2udg$d>|0#s8YxR!Jch ze@sIQ4^r{JgzR({Tnef9qmS#VhE)79DgFi9QgN8HHpPTg;uT@r(^5#qAKk7@x<&m- z=QRw{Gx{{RXo*5~h-gP!vDFL$4X^3LM3!F2i(Z3{D|>$*`r;to5PY z94Eyuu+<&F{bJV9sBk^5W>pRHG7(ifKbrATyLUOKS%|7#49L665Y@NLS%|8e)a;@f z`6(6_X=g|IU!8CNArFhRs~DrN%d|OIr2U$UMY<1CT{MIq^ULQ#2jIZFRs!cBdX{!n z;0+MHkMa;byAGRhIibtUyWd)IcHMSLVBT(ue@SU9sCj1ik&JHrNhzH-nuk^m@ zXL<0Q{gTQUz060%Z?k?DdxxP% zRu&?9@rsgIsD4`fkV}=x643G8>@V?kE37xm{51{#X10?kBDi}d$(*2B1gFQ0l@XeRa+JSzcpX{^6ci+^4>Ng>}ovUsb3n z<}xwa`GJc-5BB#&4|bgLy1$B%H>|c&d2M;Jy6WVq(_1^I&YrojxJq5o?OSTe ztf3asZ6EgOJJ#mU@~-s(^`V)1fzYK8vG|3YLnS_2ap88XCxqy6Os|GN;q}M&2citz zi`N{7;SU+oXgV<*m9h%5+$l?88>;LYr9u5qd5!V?c`0@Wah-EZ$R9LhxbYZWLyo`N z9^ScE-Mx4T_wY`Bk%=A&#%7m)4|e;_yxvnMJimwEj~_GX7nP}`9)IV2fKV&llk6Ub zN1;|0kLZ~n2LL31-{xkn`ZMnXLbt!w)tN^R% zvka4el^?YG))}km2e|@QyPTq$TD`hSkKKI*uZb(hJMR#hkzq}9gRh?#>ruaw@etl^ zd{+?bqCShqX~vr2hD%kdkL2l+7^gIcEhy3W;5qvDJLj>+2jM{am>zxqVf?Ai{vEx- z!sox8tGf%uh1MXcqCe+`dq}y5v~+bEWshK#K>@!uSI;kG(QqVf@wnk{)};-M3BDe6 zZPT2tR4{et`#vydZ2&Q5M92JESZ5s@8q1{En006DBc+;Z@T)pi@0vQ3Y6MI7M=y09 zW0~3E8Wo%^)~-G~bLzl5DEN)#QQSK($f?hy%(J_!>{3 zk|_CDv#V>Sebp(i?v_!aZ&A8i!inV(u3|HBG7r<-)PbJADszikV%i_|%_UZI8tL_z zXl)LTVqxN@PpcKN*&SLd`Ml>E>jXPAFVxRANa_EjXs7d#mijVzSwqtK5`GM+e2KC3 zVZq%itxV(JD=JW0F2ICxOyQsRI(~v@$1>?V>tGYq4l54z{<(T-!R%CO>80%@QZhu{ zQNj_(U1#g}*mUx^I<|O$Fr9=fPoCWRuOd*?4o&ehNYw=!U^}ptbk~@t_gS9asg5s> zRq|Axp3Cqz|9(%9q3;_07Oh~~^jwI)#jD#(_?y4FON+lnD=EO={4s{GB*WkQ?>%lf zcUO(S>5gcUvVgxJ+Xi8AmHlUzN8W}Z_89W!(}m_!)V@{VsDoSF!`@X=@A8)jQk=5= z>o3cYw_yITgGRLmNgy5@@901(LIixe>R`8DmBDWQ#1ZUP&(c!O?0L{wAkJY4_hLfK zEnumsI!G|LV1Qo9MVOoaIqF6nBgjS!$r^^j*3TQL0_N=W<*K zixE^|T!^{(MEBdsq534j+=3xYs}2d~<_~~sLW2#u5OedFWcLbA8;T{`UovYH$tpMN zLd?zQ2;5SZU~c|+W`8Tf+>im}n43Qc6(haPd#U6Mfg;Q;UK@7swi0vmCmB~s_WclI zZvM~|Z-b3|X&ko(D#P63^^h~F3qFYS1UWSJ1Zj_gc9J1|`e-4$AXdrF zwv9r*NmR7ASDX5{Y)-jOaZ32hM94|FE>}OPMppxkXI81{pLj4b@p zmAlLKg1W`M0FrWZn;iQIuCRQp@Ah&?0@~T}P&OwMVrPY9Lap?xawUu%U7O303OOVJ z?JOsfd9rX5_3=WN-~cT|yqsQ3BNkzaLdDx45!TJ&1=*`zj*VST#6_mtC%`2d)(hF0 zE=+z?*sKhevLq8(t^}5{V~y;&WLq`8(p4=y~{-Xpmbi-QEW%J4);mE{6dViyDIkSaTYRn8>-_bt5lQb)}{?pUsBMJ$S2M{a7++pv2uWw|roY4T?`407E;0Fw8~$yS z$z0qfTRp518gX##f#+0_5_;j^bNE}JZjO&c_KWscxa+GY2}?u?}{R;FyL#-0*Lx0^QwBhBJL3-nya$wON08l#B4P2!WRPBZq{W zE)jPX(ha5J9L?+O;fx2=%NH*b9?rM;7Q=$m64Z#H{f9&Dk zkruX>#-^3V=c-pM?r$ay<2yzDBz_txu^k>uY@8#t%Rm~mV(rBlbcWFFwi;WW)9k&` zc=rn#y%(qeH5`b9wmW*%h35*6^)-{jN<;Fm%32R+wpdc0M~imzhr5?~i|N0O?@n{3 z#kM|e#V9O&Xh{C^CSqvdm;NVW>DxG|1)?m$U9eyWRDLk;J7}FFWS@s%3{74j@kC&K#Ra)m;7TY<4?$yD#@i+$jU>@N zE<>DGnBn~8jL{|{$1kEN@#3O7K#)5b1ofrOhSEnM-?CD+j}KNxRT#a}Nd4ZxkpJ>} z@}Z2JrM=dc8Shv3E$-o!8FMNzuADq7`E4uewbf39l0i^SY3L5RN#*eQRt~RF_b={i zDhK&J$*<vwlJdu_RC9j+O81|iquXq-+D7E9Y(|Y z>=?B>#IND>`bw?h9@7G4y4q}akY8>XFSj`NDh$Z~x|Sby&yYVYo?&Y@fi@?zH^($` z7opx-xUaHC7Htyh_@vp*=z^Q#baHc$I<}{K0M|JIZ93W`_RUoe@ip9Qvof8@IaLta z&v(=^#oIA8;BNOo`-srskn6V>?k%Ae`CFtqZuM+$8;~luM!#QJ__jQPDffugGTtvq zxeK!=V4OE#45PP0e?3>9TcxB_8t!d{iV887k%r`Zi?%^_7$^_tV~QSUU~o5i?cebVwmp{s zjE>ea?VhI?jE>j2H^FFsogTn?Nx|r7O;upDKU%N7`dx=I;NBm} zGu?B_0BCiq zK>H(`(nBEtjSN)-K>Nc{QQzD9*UQ?FUkH50>%|WBHv^#k>Bg0}2cZ2)C>iKI_mwX` zxqW8m)Xtk3ZMcz2QV;{IJ17O9<27S)Xa+$0)7iC4t1+KIE*#xBUO1Za=s;ICxKA^! z7A{R`#_YBofBsD&?7zI;wJaj91@*5fu0OoJ@FYOELRS^5KAKPp``ZPfsYp}U-%iy_ zrwnBY$8hn#Y)AfQoL0ezUv#T5JCo?P)Qx4hwH+B1hcn#5{l&Ky;?{Oqqa$@0VG(X^ zhtt_$8E)-HSmuF_FX5q9;bv8LA}xXs-C`2`g>EbZVeQzcLNwSa=zg?CJ3G5D8(W=H zG~i-e_W-vCsRIqAh^u7?u@5LiT;#77 zGStrw1LfhoMJvSBPBpGrnq}%sGR0R%`h15HMX&{EWafn+tmP3cj#eP7oyxkVBn1BN zX6nw8qX)<3{f{o2={_~%pZ%hiH6JXrR)3MhzfI4Gv4O|pN$CIFwP8tj;|aWB29i|; z-I1noO$B{w=JWYXfA;v3TRU5uuRnYKp{<+5G%uvrv{|~Zv(r1yHv$XH^ zt_Te9*E6!Md(5gD-Jof{m|toB?AQN3px!oTbg{|k8T8y^f%8>GhjM&L;F{6Bvhl#C zxzZ1l8TT)nq1@Mx+$f0%1Uiheg5LL6G+XFk!aRr+|Bau!R7oMQainAtetw! zd;3l2w$3a&7jB;0+Iev8^!CHs`at-ybKZ%q9lzg@{$F;^gENL@mq2}aQCi`Q;x5bH z=d0%}ZUO9NEdLd!Pn{UCs(Tmnr_80w{~C20L-+fr=<~}}*UA1&H>vO9ZU3XeV_|rn zFRF&!)G=OqKiKy`T*pb6KpdIs5X{uRn|C>!!KuLEd`j}_P3ka1V}q-#LCL=S$m%!F z)J;WGyIMS+KQDJ}X6v^6Tef{GUl!v)YWcDr;WYd`xE2V?J5ym4)J>uwaHJ}{czS=m#L*~io& zDPJ{b6;c9iLd)6|>?Br{0ck2!5zB}&aOp?qj0Y%yV24A7i^mOrGoB;^-}{QeI4h#n8fd#1L2nWT&&>m)HWqQFYtmR!|_wC1k^XcU%{FQt&r< zEu~oe3-+JoHr2J{VNp+IAR%k*(gMpDse=s9Vz0UcTUGqz)_(3|6f^Y>Uepd}pLVyZ zgUvAsLqtiuoYfsCE<^qp^DIl=_3LqK5)RyB`oC~aJ2R4QeQW9f?Kp;Go`yp4KJAZ% z#OVk)yf-XmNms8$)rsK;h2}Ln5tlHCh+ZCOlPkEBWS9)n$CUP~-=4{wmex@1H2O9x zPbW3Y>#EXBksWcuoJYTbCu5~MeJA>^=S+88V(zHJYj7A(A4uior`Ffmi09N*MEa21 z*0|Toy!KtZ>X#)LCShYvdx(>n+CfIHbvKrLrE}SL@=np(`51YuNCam6&ljy9vd-D% z3vK>-t2(l{nVY{_`o{Gn4q5G2zsSRx?3fhKg50&tRyfZ)9$W(}abTL`+lwsUZd11| zj+F8(-xd*;$e8eoe;XvoS*M3rqN%4%Sh?^@ysSL|ulUQlMDR+q1PQ$2f7~mYKAy#b zSNsn>!kvj#!z(|{tQz?%u`Ln&)*K07W%zav04qKbXo^w=toWn%QtnvQm*}(FTRZxy z6RTJqOdPh&U==<5@2yI(iu@pU^yxM{xWb8D!TrlRROLsQsESXtsR3I&s)B805;aL~uGx@^Py3h(y6p%ju?=Wh?x4lW(z_!^ zaJY-Eg;ad{`VG@i3}Hbk!MCPwgm?Ij+=&M~LFK!Pkcv;mC}Ew?*=Gsl;_Ba*uZe;u8<|P$EdhAINOsc#sP6r2ta#zoxcTQb@%g)6l|$1N<)`JDmlWLMr~~;#>z@A{D{laVqP1jwiSh>aqU$*PHpreuG&Y z2~){LmdUhQ);c`NknRman;_SNZ zl)${xE^VSug__7EwvBv6dQw#h*L^{3)g35TCuVa(Ti7<#6tC(g*YRM)eId7SQ3#Wc20cVitatZJ)l$X8tU%vB|Eoed!A8mXE)x zP*cohVzTqYlOm}Xo_@DAx<;2M=T?wEXvlEmA-aYff3-cVbFaF4@e=M~o%|vbJrRsoSpMC6 z2Bky)Q@>z)XY1hRxwCKFKEI}Gtvjc-PH#TEb^7AA@!ALNQlG7!xp;cN)Du)2rT)m~ zYJ3Ct^{;>Z3;aXq%K&!u=Gluo=PvGSURdt*E-Wrr^COoj-T&(G-kKKbm0RaeZGFM% z?N_er21k$AzdgTwa`VjAL)(k_;?dzAY(DU;a`BO8s z@D{mt?81dpZ#c7f-0*kj6VTBdb_Q5L#S_)lT1=;OBdG0xfs6SyBv+T5yLe%HQw#44 zb#II1Pt_Nz{XPljx3^AuKFTMdYMqpvjqa^au6u8M^h<=gH?C=K>z-j8rhDUm8+6b7 zFtBb!BdWwKQb(hEW@VV}jsLA`QIzg|M7KF3U6{$`kDc6lXzj&=M?9sEyQ~dI!g^1! zxpiT4XZy^Bv*(SM+^}BqIqKPqXQsX6IK}D|3GusU>iMO#9oH%UP8CJ5=U+Ce;5pTw znX8)$S3VvOY$epLj)P8Ti?kv>Whlt@)qkC-{62N5+ktn}A@JnRo6qT(dc#bTLSP}e zi0CLNf3^wAPd@TQZ%gBn7u^BpI-a@!56NRsSE`Fi&sxMA+_y@U;=MDq%HtJq#XMeB zD!haeUBP}aixS#^s&e@t(r%c(nlN!|A^%+)Q$YabnWJJEKve5};zIw!87xo0zZ z8Mtv|gH+^k#NIfJ@DhXm!MS=alT^6P;RF3tq*L!<#MP&&eM6CB|Ls>_Ia^%b7fLUWbJY~w z__H(h3{rEwl?N93=_ajt+>v3@AoEI0hh#(z$D%#aaogii9I2Cb zS3)+PLnZjFeq^Se!{Q6DF&1B(ft3)GTDDFhZssf-%a1O#Mg81NJ&U*Yz^0I>SHe|j z8TvlLSQnJqs?smpT+br}S{0N2u2{x*(kUNZTyb(tMW})mwJ;-{^3fF$f91LORIvVbwxzx~RXX#@fYFIZe4=y~ z3?9{IF1OsV@m77)zf2NAGyuRxG!M6w7JrEBgWB*7h90F4>d+7wW%@%I2P9{O9POIf zL~9+xQ#iUJhKdoM)zIB$pI4@OumV`f@77!r@I&dI{I4k^9?Kxt2mrs0rQnEuU3Als z*UK8xe|&Gnx35`IFKI{J7~v+=m#(=iTH97#l>18Enx#}8L7Z!2P%!kY?q>K@pO`1` z*kqj3t_MWAVy<=#?IXFvY9q1g0I2>kb%ZI$b+9ih=6Y+ZltjGb`Lu`uNg&0P^pD%k z+b!y*#bM#*jYiE5tcp;x1IzPCbgeB#Ti{%fRJRF*k-GsHz=1G3r>k5C)JNv(R=y+k zg$Nu+8M`|AFkFhu2o!gPnmy2;3v0q*+&v^1iMTBF2Qzh~jJLo&l=0U2h9)dY5U-Nw zol=6ta^F4Y4D>k{x)PkG?y=?$etWKND^l4wlPG0bM<1_B^BQpq+5v-2YGb1i`w8{m zbob^`4g;3 z=suyax=_e3NgX|yKE`hCHlYREn|K19T?(q^0eQd|6moa>yn)^jQP|u1^6~PAwb_@U zzDI0UaD#g8;<{8;0B0hwUeoMh`S*eGkH4*6Nwh23r`|JDcM#bDoS!&1yms8LfY;Dh zgX<>t157%`T6bf)cTKqKG{1(wl1ldSVwm}JK>z!I`YZaJJj&r9b|6_`4JA<+0cC8n z^5x(C>V1RY-pCuse$QNOhztbelhma;lxH2ar$v7q?>*Y+2_7xzifhd6csr|pOE--a z@e#xf5g*&|K|rzkBN|miWryXB6qZ_JDL8^!7c=zrM{2*rr*%(6jo+WEqY|aWq&CJ4 z3diJg1R&|;o{~Re&*L_>P!~HsIU{eS5!3|HV>0iKY$8@`tPRE`^}aa?&k%&eoyc=# z`q@?MGg1U-qe9hTMg5L$u_|K+Jg+0OYeGx6LPcPs%!}{`5HngW zCpv51>ic!?_*^{j1Oq}Sb(;v`Mn|F7$Zuo$BcaUcY&n1AhN&-cFDuSF=eJJm=w5@H z=eBkpTsyt}@b>9Vj1XXz0)Dk1FJ+NBv&~kwoZwTjvz+}cLZ{Abv$ZInj=(CB)V7?O ztg(ev&y;X}m{H~QiGp!c#F2cJGngvc;_MmQ{6$7hGav<6r-*A)+h^jG?0O;CC1{pp zr=%aMnc&jhyms7B5wzi(^PAbl^PpkXO;c;IYfDlRlm{3F>M{$XM>?>GUX2Y2dbb zymJ1!NRI@>rA{@6g2A#XXy<0of#uX0j-bfRv3Ilc5w`TCA7N|9^z(~suLu-IEIS{Hpb$MGq4h3Qp?sU1a)&Y zST>eHTxyHb1wg7#A6R=}Q#JM9682~9G1bKp5CW}=Nk7@e5e^@D%{t8!n-R7fNpx|9 z%Llf_<>^{?2W8PgK9ZyN_}5~~tj9Q}1SGphnJ^H!DU;^-3rO^WJR z=zl|YarA!_OVue_0l`gdDbd9-SUNBy4o|9xOBqq3i=)5hRD>#EcMCuy(Zw-b`QA39 zL>I@93}{MpImr1w{fsSj2^I_<$EMU7fdY|>Elef4ID*9kgW_>!GLuSlaa>=Et}gl& zkl3lgQi=&(f+jEYoCN0K_qkf$>^Z9c_>f-uW{<{m=({XZ>Vqm;8=7m%YKbn6G?xj0 zXRNZlngrRUyn^Sr8%x2NOB*>zigJE;z2)_oqbkwGF(U~&8qk%JX#Qm;4X>BCVM~;G z0UReGl@764l5TEGc zm?0j^ARl|tEz!lXg#JAi-x6IM3#c0-9M#3KU__`dWEaPhv3psxwynA-m+0b{rSMZO z&Lz4yW|;va9bJ`8W?gzaMRjq^FG04$YLiXWN4rE9$2`d(-W7AbwN*+YUh;feq%ACv zOweu);W2$%>Zq)XQ`kT^OqTR&NR2RoIq0n(qod(mR zkX;CQQdY23n(0z-V0mCZAP9f37k(xJ75SV8Ij$&1{7L3Ry5~(hZNiv}~ zpl^Xj-oR-=qT@#5t5|oO!na^r6E~3P;+Wg$!Gy z1&J<>8QOxZVDI~YL>I>lq0kEm>blMg=-NUv9crujO*}!Oi(`(vz!wy9w=GXn-XPJ% zabwrIIKr6-tk>_zwZwU4SZ|_>Bb=W=LDzx`xS$6T9DlV2*G=pP;5-B!W38LQT$!Up z^=+z)BX>Md)2`vKq>}A-<~!QswTUi{C@n$ks9AQD`vE*PT54ESqAP{*hw;W&CAv7G zWWz{8-PA=tChs?+zfR?kH4Mk2g^-WnQG!$#N1QkiGemrJk)nnV0*X^z98vPHyb<~i zvy-f$A%Cq-9y1bM9Qm~FiKvn2;>f0Cj2l!JNA@^S`sALHKjNN=9f>ZE1T~)`(Ie5t zksut#kLRAiTeuLPlU*E%-y<)9Y2$#f>L>EU(A`77u zbXo%gH>o-%x;TRA;$mZ*g-M#;f#G%2#;cYlv~}vt_W8wpaT)C5D8L{ST^zxz!yW?w z4OB-$>|3&D2KHm25m%@NZo{dqbfSwRN(wMo3D2ChR#2FtCDFwZqXoEV#k{}*eEWVvJB2YuBiz7}JjJ8xSLi}s8D+2y=#7r&jSHV)TO=4(YqKhLz3@}s~ z=O#DRL>EU(kE`6qk}-BF=&=@#+a!u6x;Tcjv4UthE$yr})x{AZ3EC&sb_e;C?Ba<0 zHkQ9r&~mOdG0a34M~o0)l>&aXpg0vg6I~n;LQ=7_Tp}$(XQGQEN=Fbmk<_-Fnub=H z=;DYH1?@6hopZicqhwS!PkgHQ)UA!0<-BWRn~5%tI4Qt7MO>TOKJ%DY*bXQ5nXrOo zVyL`+X=l1bn3tWOz=~=yFFXFkn3tW0po52br zu-$~IByZ0^DMS?#2Idu#EsO@?$GK<_{yO$L&M(&Z89W{cDtFkXD)Dj|6vAMjviJ~k zrVSqi6f-cdaD`Y-2Ytt0jTw2&i$QDe*IH0mb5bIYdF3hLVP5%hp!9Q?7h}Xd6Fc&l z7o;ZGDbzT#Xk)OOp(yoo8S3@4TL6Xf3`zs}mczW@?~xeN7W1+bu2zQ?2IggF>tl%K zCWsEz??2us0MDssZv)S^9o4;1}ouN6ADwb#LH!N0%GN3VUl{eEcba-kEC8y&kh92kVQPK!>;tLU#g34xcb0Z7cCSfJgd7} zZ%n*g77_{_IFCJK`(-eAWJS(-6)_ce%>sM|%dGTno=%T~KE@!a;_<4IWC3xhm&-!I zU|AKkbF=d)64Vq|tbUAsxr{+wfHM`*R0A+2UM|b8eRSZ|%VqfiplB2I3SZw*ExGhg zJn?cFWJTb{k#lB6da!$*J@s-KBo#Vx>K!il9rRGIT?GREd|%d?KJpQLl9Q-;ght z`9DI!RH{5%K(g4pRO02bVCle+I6QGyR6t28L?vD>^Vgh;Pz5V$5ug$;mxU`23~3s3 zs7Uz|FPDX6KvSa2K|YL;D4qE~+l%Iuc)1L41{f5NGn1KA;^nf9OYm|TJtu*A_%D|& zI}<&k|M>m9ieE0HS3*DuAqXOP%z>V%3Ke4+ zWoxZwcorvRIB3SCw2OaUp?QH=RmkwxY?3fUC7v(K$Qw%|q2n@zk2a=>BSUo|F7bR> z3HLq2mjY)S5bJX)?CH(3=>*m&l&7JKt7tWq1pD!yJyO%|4BW9l&CD>P~D~=4mR~qJ~o{l1?jv=)s1VNM69R$X7+N?HApHp;U>GE&R)vbIF z;tLYEk1~dl@0g|ejO5Cxif0o^;I1Ua@1-6cDPt~h5@pPF9-;{YlB9lYrszX5DTjb2 zO8bV~qsg+{xP(8RDeBp%w5o!ifvK^o$U!Km`+DQhR}i_Xu7R;4ZkHSviqb$VX$WsU;D60*-Dcf4WKX;1jp@HZ}VUl$deI`xzzPu7U8d-rRJ<_?hV z-^>(sGZm#KsBPCQJc=BjW9m)hV#5NIh)%2`_1?MKAd(@JbQn=Yd?W8c<5MM~M-9{Q zEK zD69ch|6s0;O4JgQ+89BoSI+nxfdDF1rQna)^SF&Q)JBoNo2lo@I32_h`j_mtLP50P}CjVt7i_3ALQpnFXHYAHE+RIX>$E*~65G)d*ajt@9 zVqoA#RY~1mj=DYvCX9=bh~Zbz6WM)5ty8^guGVud%@+_bX$ecn{*M^{;zBi2=yMuf z!A?9`OxE3WlbjnQOapnKglSDkOcJBJSyE?SFu|bN!-y|dNTE&1@Iy02HSNP`3F33b z@>|*@wu$?Xa}pE534^;h8C%G%3dCfQ+is$^a?3<~g!<)~$iu~9R>ry}lqU^V*S)AC zCRRMIh8a6j6H*0j#lmqLMO58SnQB%O&c_O<<PVJ%2b+NGw1I^}^- zl8U3{GHDY=b*`JORXHUFcpH-JmebSJHh+Xs3H8Z>F;v8pd?hrPDiJ};x!1-*-^VC> z2DAVh6|rq{EB%9+%HSm7gA1|mY**p5GhHHl%+62XN^8f@p1H7N@&$RV%8oxVK4#}3 z=-}aFxly3pbNE#G;cNI?TiMLxV<9!czG=51Ey-(WW8}4~k2)%;JU(VW9@Gou zY#H|!ew>R6;jd%g(?(nHxFD+RVLPonJ_g8>1&Z+FHk=UA%)rOOHDWm)^dc?1>!rcR z7_{z*_>srQ@|5uKvHUnt`s680=(l>;gX+^BiXwS@407mGD30XuF-SbzUu5tc_<0T= zgTF_jN$QeMO*+ybz^FVv77+;IiIAU{BBJE+vEad-ScE2g$ZOYT;6~NFJU(V7>|I%4 z_!R&aQY@Fek7nKMc;6A$8yNUlKo&IV4Ac{Ro5RO~AIF-2WModGE7SwGG|Y@vC446qH@sEBQoTWR9Wv_sRsnP#V$1QAQTnPw*@Afylq z6TApw2d=`v-ZLlLW!MRbm5-%Ms=ZL|^QdA;pXZge`YhsMEzYIej!+$ZLq-HHd)EQ3 zBHqC8puTsWIK~kO3qV>t7Lo0rLr$h#cM+XVJ)3tGI{4;8I@3TP_vdK!be0Fx@BsphiQ`{rb8iu~F~S57sC$`1fVo2X;> z8jorTz@3UXS?kVq*B|PsE69w%p+8>jJGt}bbK8sg4YO5|X<_x|BGRF^Ud;y?_0DGs3+f23x`ge7%ia@&#F=d9W05`?`k~f1a+F* zzefg4jl~#eXC(xtNKyZZeoZalIu;2{epIrkHub2Vr{7Zps}G`J$%3P>)fT%||Mxs` zgUeGOEeK#+)SF>|1LTWp;CHF`R>HV8_NBc)jgE%w0T#w&&xAInt1ndF)kCe{R2v6u z6u0zqP}DlU661-ex7CW_Vef>PK!*$hGXjmN5$#>O!{valmI)GHbLAjBscg@r_ zWW@5A0?diS6lY5Xq$IhF+6{(sDOf&7N))>RbKN6 zfd<7RKT%!_22bRH1ssxF5S9l1hg2;sSUoT+9&4sEtiPWr>eZ}6m*9OhdQJi#@gJXA zcGJ6T*64jTdL;zx5CSHG_d4jAst}a7(eJC#GZM6ob-2kirMs7mNKtQIzl0G7*g*=; zUfRe(QnqvNtC?w9+8ja?w`m{psrqxe`)f`$!5*lP zt7P}r-1KqmNhNe`go8&Yx`>N${D065VRIydfK|xx))pxvKk>*~hI}legrI_{hqMtl z4j|RVIo&s#?sS**KPde@64I9H8i?s@=||QIq&vns>XEgA5uv`2kF1r9-OHl2b=AhZ zx6`kzWoZl|-lTB|gSOh(FpQ__ujzNzvJ8Q{DeA$m>_Fr@YuPX3Jgc?FH3)LpzU-0W zOhY?95c!Jv-kMjk*r$CzeH1G4{a#5m7P@iyf;Wf|)f%QiE0uHMOKbDgOKarRdB|-Z zX19sb)EsjnP52Y}ki<)CX;PsBp^MTfbs%3_%S)L^aZ&GvfFMV9BA0lsdj<)QHhtPQnKo{6kZejBv z`O;c;0kLbaZznY2wA>G1IYi>6wJe3eIh3+^H{HWW=$FYcW$`%qY ztx+?_$wCWqTKWJa14+HKmLwGV1nNPsXYj7TdjRboDCsb}q%H!ZACvc|(O)Cc!|p9>IE<%^BDNu6OeH!= z;^cwgA>yP988w^`(VXfei4utAkJ>0PtpUbbLtzcFn&>3S zreusD6#mQ}2TGsZW%5VdLs2BrNs^!^h$EBwd8EJ~=|~LMAKxxYbdrQ0_i2iD4q$Kc zTuDBmOP198fz)(yr80Hyr^Z|x1ejDONs2%aPlWuu6cI&xS*kOGC6a|^3R(q%fg4pN z6P+Z%gmEzv-bNB&2bft`@B-$ZsZNp@SX##0i6j zO0`48za~2{;6F!<)$ZdeI4HMFY}c0PBuP*M%vHv^=?yl~NzxMJYP7LzjI|0{zJ=p9 zil~WBlHq)DKt(LIoSLSFn&>2n5CyH2YW0JBN_LV&ejDomVkk~=%ULP6*iTl>vzuU z>fI~#>K;B-ymz$`w45h$b42`tN_3LMX#qAWV%y|an#ae&2OMJG32R&?p2`afcBV^& zkJW`*b29Q z7#kn6bH&+Mjp9dcd$)_E416pkT^JR@k8@EW{B`Vm5dZ8vW{n*1P$7el^7t4aPZlV` zkK2$fBT%0s$si=*8nGM?dXc>vJ@WV%gVsuX%+631Kl1pPou~J0nACXqSbiKR{Tx2V z7;z6pkvu*I=?QiY$gaM|rR{ELQF(j}5)UIu2G4LiOmjwCL1L1aJU$i>gJuu=h9F(B#5RS! zJU#|!3F33b@}pSX*5p+l9|MHJ*qZ@)z}Go^4E%gg8*3gPgVX?Xm9cJmgU#b({>wM9 zY)nX%*ImbP8%5MSKIYRASn4j<;0@h*D_41Z%qI$3X$J1`Kh5D|{%>O)NCwk##VClS zIzwDu+t@k3bz?ezA;+ov~e8tiZfm;wtJ<_xo(zGNWPlw>dwUnZ$I zTE4(GVKk4Ag_H!giR^OA>B(DMcy%BtpgcYnk_D|Z1N4L+=PRL3t1(?@p{{asMEn8+ z9}6jmHY#FU3$l|+pl_B_ooEhE|I(VBViH6w@zR=|c-Y28aBK*L3Eoz*16S2B6ECgV z35b=CrOQ@AP3%%-O+SlxSc`KdURn$J7`W^pof1I?OMh|U!L9S#CpXV*J)~duSzNYS zM0n)r^Eg@RrL~Y)=*X#l$Kh9GE9mfNNOEP5HWaY}_u68yT)eQodHU>$FVsoZVt&m` z>3u$(Ed{C#_CGvsImcZ@w1~XaOKYKUu*eEA3bP+7TK5!Jw{)NR7mz8jFDaC|z7$hM zQ_W?q#7k>=$&9X?dTA{`02FPaj=|$&rB>Iv^TbPQkQsqPFH+?K7aS(vA{#~`N9v_D zNG^2g)T?3eJMyJ9_(R04>sSYzYNdKK@boJ2(pp3kaO_;3RZ=NZ3#FYO)r8`3^Ox4l zh>a>*iI>*QOffhVBwwkm3t};=f;W-O3L*?BzPO`=PNUQY}SQwK%6WUnfrM1JYUs@Xn zY!tT=FRl424~(lSYzdx7GL+^GQzc$n^T~jQMLjC!e?z{s=KlyWsZ`2Xzyx^^OTDxf ztR0vWhbhjM3P?$YEA0lTE~WmWQ_-qmNo|5v;-$54?SVNVb+iHs@ytEb) z0u72T82OM#qP*t+3^6L32NrNhZb29%l6Ywium+eFk2TX7)_5Dt3&Ymd*Lj>(853k`f64*%H>oXsmsk<9d9#q&`&|6b*d&^3q&lAqyG>|fpesoQ_ z?<}MR|0QKQ>|v|7&K1>4m(uaL4gdfTWS-D>(`6OxQzX8$;2aD%N?%5JUXhuBgQZ3M zOx!jQSwdUw!b6T&e6SSp$(g#Ph|?e>6>+*PheW(ke@nj>nzS}7TZDjtsdcol4ICP( zi(((1se4O=j?%uT;@DTvFNKPwI^sywOQGWNpq7v?g~~_nWzg1|YNOd#((i<3=*#VB zT38i~IIEM!P4%br+g%xEz+DlIY9wNl?z2F3z|Tz|9r0e^@yQtfaS(SY5!+ek81UzDU? zJ&b&yZxtZVEn#X?(vw7fBy}tmwak78iRE>B&iM6%N7RFKDFK3aPPl->48zGAXl~w7 zsNa~YTMNuz3lZ#o%h*Lej+o|h96p+=z1-pvAEdMnm3Y=TEhuAcQ;sAZfv!{1?@|Cq z;OgYD42>5!YEyt4)uF|@CNaNo+bJNBrVNDs0*1C^Mbh4N&QHJ7KvI~B`}MNJfKa)` z>ztomTXe<%qJBLwQ``K17F4`_GGWXw;uXK*9Za354b(HdjphHYpu8Q>25i5Gt-EIZ zn##Mk-*j&4%t>(f^-$egX4s8O);t9E3g4o6L_cQchsqV5C0xCE_TtXDi#wb8LeRPG z3yUk&P7aM*??{_QdJbw1f zg`IlpYNEq$&(&>pRE+VVvCO+ByltWVwJ|~;ceqZ%;UG#Nr(epgpupApsnSf;x_GAa z)%Xnw;V@2!xYku_aSKgp5Ezh4H?o8< z2&VWqsfnr|(k)K~L_w33Fr=%~f(B|Tw?V`gD18Bw@5AEIDPdUynp3K!auv6*t8|Q! zwAxkKc^DP9Wwots#m&w>NyW{MUeyySS8=nm5HxTqZn-g_w9^&0>}dT?)oHkN4`RnJ ze-CB0Sh@0%e8tVq6en8ci;j||RE=y{Xc#k?8S(gOyy?qU+(NRU=TFv)!cWr`xA3>I zk7=PHxZ@Xd5Z*e-Ronn+AVU*yumul9)T&&?EhG)g+Mu^ESzNiv9YW@qU=7$z-F#VtPylzF=1#u#tU#E4wQ4bl>1|G{=#WQSPtUv5AJF(+Gb zgM`C4k*pNKkJA-5`0K}1tjJZ|BH}=_5b<&M<3+CG7POU*1!sbTw7g=5-KC0~nJ!KQ zdPItGEF==$M=078YWQ>CT&Ow&Dhef+m?PAAxVv6*ur> z#4K%Zkz#(xZ4mJV*@_#a1Q@1-WmB7`KK*qhRd4mX$Hx*eVe&=`OkJl|7uRHgsd3as zQBpr5a}AeYT zGpe50l)G0tQ%#-#^M##?jir)k6B;!(oT(rA=YRt%40fX_R!wPhQJ>YB`o7EwKp+@k zhXQWo>XAO@VjqkIsCtc+o2SxAUh=pfU>Asq0(R9?mr^pN?|9uw_!Pe_c`OK6Y%;+S zYlN1bU=q~nhv_pY8M=akVgWTZXZL=5t`4V}l?IsFh?3k*ayaMowSafUC-X)uNng!; zI)i_-L#{3l77z^+BG^VGIIlet`WRFu`nP)q>~LQ?+LgB zyfcSORpg2YQ~FxoT@0nhvZX!lq;(5(>Qss^AC$67>85{Xu5K!n$cXf`5-3??;PJ0Y zi))?rAI{V>SzHX<^=9YdlK(_bTfbdNrN@9*z-{w* zRYkJQaclQ>h){6W0q7fSo#|QX8H=Z->P$^LB>nVEJuScPz$zQpmgJPfBuc$~P9ER( znGfuZL3dXqQ>S)v+HgZlZR?O-viiZ9dUjr_ux`X)NDXNzBGkL}`#bp+W7&b(jLLc4 zraq*RtY>gm9@r29V#?PK?I5o7gy-0j0{F%`dFI-)AuRx4MJ(F8LfIm^U88|=<5Ie| zK%tExTR?zfv89HVq%ifWsmoigA$QBedfuXXk(r9!FN0mdI z>{1a(yqq=xNT+aA6~kZm-Zq?f&_ynv2xvMS!pS06FmQA=u|mz{wu0EI`XRca6|5XY z9u7+?XiJ*VCuZsa!s0?xM%}3x5es}bv;#)7jHd8MVt%4}*hy7;i zX{|vg{$@`0M-WMJtbt9U6#MJwR zngI2WbM?YD(i}t7M7q)4-DZ_Ta?sLvD#O&5>DQ-9NDBhh9tk&LolN9aeoyz|EMWuO z$rSOjYh(g@KV0dZML|BGofrsMMQmjqFYWl@Uy=_W-T zx``eQT||n&Uzn?#MEdQ<9FLn#p+Dn&Rb0X;_Hn1hjcMFP*_x)10DC zXJ*tXKAJi#G22rH{yQ}IkfJKc3yQ4|+rU=K5IP6-EBe75v59NJ0VAiX`YASbZ8T=D z8M^McFL#=8crW4TE*bl;(A}tkyj^vd4&}GmP<}|QFK!To@_zItkz8N2rrI-+6isL4 zwH%3zTz9}^R!BU70Sg!NhwSu2ax~VFq;1!-Ngl&>?-mMIqlp4(yO@v8sA~yM1#q38 zQ=M)}(o*recCC%_b7ob~sXcf-BPc&7_=g4SS{6VvUWKWIdLO&A>=OuMcL9&MrR8AS z1TI<2DzPaej%C^3R_rC?xLv@_-LdfBrHZE;yYx~9oQFU#{ch;X-b_mr)8DcdzLWuH zXCasDwmES0s?JQySMboB9L_?}FqVPwaxHTV2n-O>oI6@+PbN4%(zlN;LxC$uy27LW zKkAq^oG7l<0HiM-+*i`ss$^MM^e9(77&p|T)FoMzXc!*wW9{gt#3fnux58wvoE>;< zkcek_mkGZ%i&F+NJ^=$=Bq*m>L>bw#52XxC_@JXO1)OpMcrE*6VL{)i!_!+nodIH4 zPF)RBsE-oh&kL}r7()a8NCtZpD03nmNAjpOxLc}woUmrD9ylc>W23KZVxXlJ(LYMgQ0zE|=2oxeVwxC%QDs&$g93HmR zv5-4R_L=^yiMH4S5VFW?5<+Fn5t$M(t@Z0AY6W>DpR$ zw93ts9UrN!twyK)pQ4_8BZmxrg+Ut~&acAiXfC72U)=;dk!J^Jcrn}bsn z$-41XGT{$Nh8{mz7YaX37uLex#@@v_zjEf_>6TDdVB4lXl&Px$!azzU;9m<42xz5W z!azMGmZd=-u{R?^uB;Xc5T&!KtY*ir!dpI;DXZC8de4PX!6~cdM}aa=m(>{K?UBf! zL6>A{15y%X`@zOrV1t)BHt23!_z^D31%UH5B95hNYLLH3glJh)v(v3s2MzU`nUb2F z%a|sJ5h5<`c9h6f)Pfe}vCvFNkZxhku)9)GGm~YKzKM73W)=Df%~dDPp4r(tb!Pkg zV!pU6K2wvcsF|U9f?!IQtP%xZrYmZ}Ph-h1eH+N7obDE@18%Je$z&^PAsN6l`MhdC zTZW2cMJ=ELjsDV?FKh^Cco`*EQ3F&2G4_&H)Xc&n+sszf%$LUZg9w_e4}ovf6*cf< z#31FbUO*iXTOZ;CRIZ{12>~W4;n>7R`4iNe7nX1CY+)=96B^n5_QO$|M8;pDnx2Gn z#eosQkxS{whC6ocTzx0?-UUJq42yUpDda=qy$j@*vCI*Ba5`GbK(SRS-TDiE?gAqM z*dLou^@ODYqRvZ`=MTXv0TI5i7pzYsy_x!u^B)b*e3$B3)=<^=W^0W;LC^vPyvWrW zgJ~k!?-481tt^u#_J9flI}~thT1)&Ob**w^t*q@7yDhYxU=k$hhv~B<8M=aUVF5KY z*wKHY-np>M8a9QIQH5E`BOE(3(LltlYx^4CCRP(=(Na&vNPIuuqMDxG!M@U*^wJr<;W`)nd(_(&kO_g4Zi&zo7y(Da0qrDlgNd-xl z67Yu%%q*J<;(+*e3i);gwF;tGAKp@_r?m!g`0v!)6+)WE3fBWf zSD7lUAl|MJuCTZ%I5m&xHjDpu#ex1d%Omt{z67^|WC(#6>a{8I-~b|tDB(gR(T7SO zi^m&PQ!F103Y@~i`(Y)AAkV!gI3Ig2=UnT#jzdlIrctMmZ;w15AMQ&=@4yWG8tAp3FYhFPn z5Le)exZKj>B+-g|u_BVMS9Ofj5VL7ww{9nf$0~XUWG_}6pkJ&YCXE3mW`*wnR^h8= zL6rQ0g8czSzF3hW5Ee72JKeyodSWMDtVm88y8&BRuVxf(@^rzi7Z*IIL%vv%Bny;5 zeMP=lks%D2dI?8&$=JykD~7`w=uILy z`CN$rmdUQ2@Q`1`I^q<%mce2>#%fIDixnw80=pLT zu^kT*p+S)^RwPJE#qU~qApl}Ye$K4tIjseFVl5fd+BNxNMOtVeb{Fu7TT}M=y63H% ze6b?UO<>;z+}s@tC%fvc@6wAEa2^7;^k1x49=Km(mwt*{_+kZ|orPDjJAA>>t2!u= zUG?BB1Px;a_-+)RWR4NleQ&QC4ODx}`6{XM!mfG;=&pJwSAu;Ufn}B^hRY_<42oQ{tv8`dcIxq;_%TjK}9i4eA+U(TZH zaDY_>$gX-6X&^?3xVYOJfwhlaz35VxuJc359)hV^P2)+88JMR=S;lQA1nqmSj67eO(CeieslG z3Qyr-{{m`ic1UDDq1>v+jS+r+A@?y9B^fg_mqWldsDO8+m9gas{R4DAp^yQAOD@>< z^pj!9Q31h`7f%$?1|rFRLLrgRN#mD~!f%M{SmBRiPvAUM0RwP%%-erjLBMlLWIrK5 z3UJFD9#s)5AdBuN6w-j@Nzk76m}@5c2{9-Os#pc;R^yz>enRugaQ(vo#Fj+f=`HkLcwohc_O<-r-O3(aRW=RU6K8S!i58CV(_DejMRe3 zenP>5v1oQ_ZHMe91lAoyGNb||SBk7rgt08ee6pVqAOhMC-Z=++MD!B^zuJQ~MD`Pc zO9xiO;>uKZMD`Q%J6p%hHb%tmn=ua8B>s^7g#0xJ22>M%#3W`7O_BYCd;*~H;5~Hw zFNl6Z{!d~_CPR;yc6sC>`w0ch1}2l|g|ini4Jps36E?h`kiXhg)Df?y#c^P=pHR5! zz+{k^BVK#f68Is9>?ahG0F8z!&iKc3$pV)DBgAklsvzcz*h&y1Rb)RQz!6|N9F9z6 zJfEQY39avPKOt&90#oq&3GEkS3S>VaY85Pqfqk8unx*QTF4<3rnvI}otZ+R*oUjG= zW&Ao-Y>!d7>m^M_1+}kwzDv*wzgxrp9<3nz2_-onc=jL}qDxlM+}Pf{o*hw)09Zz} z47#6Ck_9j_;I|c$Ly#x_M0)C+Yd!PuKq8bW#6ie@LP_TE`Uxe6XvrA?59oeE$<<<6 z9`r$Gn^Z4p{Csd-WcmQrPe@L8fUwk4TLZX|{e;9+jnN73CnO#X>ITtINIvc!i%?`g zp$ug~j6!xF^;#tkHR1L|;!?y|7s zOfl=F&QF2p9aNnY{KN9`S{6VvhPR&fE3%(ZT4*447x0K%Q}+3~=dGLUCzR$UuR$4}n|y{e+hHpqALBzb`KACj@6Fu)Xj~6#M2n9K8ysB>M@$SqK`& z3NYSJh&e`7_r?1OaYqBy-g3T5a7e$o&hq}ue!8C!N=H!JEnjlv#{pItEz{R_Zu6j? zJ|qg!{e)1GVb+KD!$ChK`U#=G6~=;c-s3qQT@#OFp~!whIAI_f2>9osKsmV*!pMF? zC}CKx2YrOu(Uq?s#|!Hs0@+W9PiKG=mQz=Q50d?a*i?*B0q-Zo9tFyr$OQT0?UBep z_7fr~31UOAOAy!~;;;Grgy6#T_TZ`$XV2_xojS99elcHMmU;?)2!~`pAvkuQXlNGz zb`E$a9O7f5pAhkDBtlqRuiOHw!vRu|P-H(LiZl=-L|okMC_(lUvc!3H;aF%UB(Pgp zfMJ_d_sD)iV6utJdc{0|hE4Yq!U%#!i}%REz9f1hU_Xr|k64P+-C|bY2An9&(EWr^ zGJt9FdDVcn3>A1kA&d$vSKAYYNcI!LsR&{$a<-;ei=aV8m9=mOmJ-YlQZHrVBtc`u zyW`;B5;x)TA0q~7<7YA7#MXy60lJ?MK?pEO3CAWjO0u8O!8O^TZ)2S7Tj_QJMh$IQ zKarc1UDDq1>v+jS+r+A@^~#AFAP;)7Jvtl~%@b3E0uI)>XtJLWWJ2Ji3u;q?mqilp0+Pkvo$e&96ADOxc82%Q2|gkE2?f7NS(uzyJQvFF7~UZy z`w4{$2iC;khchKPWeFM-*-t1~Fc!^BUL#*mi<4PoKOwO0Ad<iRRI$|cb2s&gxp>WlK$<$a8*sI!!3FYcVXxU+eB>&@HeGZ2CaJ?ul~2iLICL;PU2c%m)_ z&%8g&Qoz;Ny;!;&7k#e&$4ue39`H3~_DzDBk*saCmSK^Rf^v0C{s-skCiIG3f_!Mf zV*U}gZ&FjnH|g~@6l{^4P1N!cw|;?e)gRJP2vhX|vjToV%-41dkdXS9Gj)elg;Mdi zR(QcV&569mKT@}M5;~l^y(8fF-tZeAovT|4gbvQ=;rCIJJS5L_l4gbJT02Xrb`?u` z0!OC)WG45@noQt73b@;jF9MBL@1Ln-9A#n^T%WHVw|V09)`bg;4Rvs_cKYmzBS-mz zW4m88$Dp&QtCxo^*Z5P$DMcj-Q5FDdt5%9VURMXtJe<1%ABrO z@*8&nDFxAob0|^n&C#)G=Z3*-Z?(*wTo(Jx^Rc)$9zeudlTE}S}j_Do{PU%Gak zx5htTQ8)HuUOabF^Ko-|FXsH#iJc{5&u#5IsM-AR_Gzsg7AHS*YPe#7~# zlc%=N>|9vh_{JG>{Op;BH}2S6{&H>S;#D+}; zpQE0=cxImpl7o-jUR|h9Tc+b@wlC~#pFjKXVt&nKt8_B!-^sK`Q^IuqGWFpVb(ke_ z6Ge=aeSv>Ax$@zw?iraM4B~zk=U0V;)`m5K^C@MS%QiNn$_2Bo^G z=(EXn>QyB=RU283TZOts-LyC?RH&j*b3pxv%hjD_`jdz~bgmWI!4QlN@zuZu9*HM& zUrnw%pIf3k`S@k8vfXagj*`GKODTvb0xo?TrpmjRSZkT@%r#|#Hz^+>bZ;SQfgwSEelHiW~G0#TfOCSb*n@- zQlW_2S=dF70qEMCtj#2P$J<>m`0?a=aSvZFYD3H|Rw>rh^^0p+N>LbC{%%D*Poe{( zhsL}EwF|K-(0|*l=7j2kK;DnI-%c+7m-6K=2oqSx5Eg@41P5j@%6kMFlRH(6vY(N~ zDEo^c4$$Gn7Q?hxm|W2LV$}NbIx(jhmD-pH>Qk0asL*-!slnwli&5@{RZQSiG0J>J z7Ng7$2659N!;1lvOFLhT((nHuY&0oiblRF=}LwRLY83j9Oodwrkd3Vp67xQS>p zCtr+0)>jw51obIo?p~0>fOeEyjM}IwM!~PiO(gu`pdeH+3jOuuijgly*>yDtPl95U zowh6}`NgQCeNbW+qwKt?P(8VLzT+FY?7GyDU+N5mcV+1nmL~ zvlwM(PJ|O`7ZQF;7Nf$S4GKUNwUOUWF8_Qn%C4#|ObF^zcE;TZ4z?t+W=}===Vho; z(zr(ZbuKvAAF_f&`0L4)fE^sz^(tG$kVgVbJDL^nYE*qBz%ou*a4=;RTs1Pm!F-$x z4(4~P;1K+Ha=l;&2VIz8RJKx85P7^XtU%R-u6Z3*vfyA#QrKQ26CB2GbHTw*v8;M! zf`k9t$>q-u4z)Efd2kr{AQnCosn|##!=jK1`x#lhw7(e4oenRy^0qgSiVZGOVLiV9 zmat4g#aZd{nMlQkZN!BcI)zl2uSlfA{9q6_9Wu-=WH+RpM=Ca|{kZ_VFf2lapK|0( zq(T#O`rLzCco3(Mit!^7sj!2lkP81tlRN7?Qn9gnNX1CGSfn4~9h==~gjCSukVpmn z$zb9XQjz&?a`ng~703!3l!63E1+sutQRv#}I?z+qXB>HwE&};T#nHvem5M2K_tmBdcE0!^7@GD=+ zJ3DJg+Oa49Y@T{FW z5mKm;N$-U;$Z*?si4d0P_v90AZ@)xN>9yg!woZAk6Pr0V4SE!u@9Nih?LIU$a@=D4h{P@IpefwK|Iy?T zJC9o&V&E1wPz$b8jAV)hhy~~m^MH#(jerY!91^&oKN*ai0xmM&O|Bn#-~w4;gR+nS zxIh+ARv>b~g((otxyS{w2IHKuERaGj$YDt2g8X7oD+;+te>S;R<&g_yfubFQ0H#2u zDl0C{G1tUIE|5he;tz#fP@|B@1@*z8Jrr`0{AzOL$s-rY`s&J20^|ajyQnZVS4`yM za20X^za|S)@P~tjP{;-P>&Z1EUzoD%Y7n9X$c3G@EHDMg#e9ie%oVdRWoJ%>BWi~M zJ|+uO;CF*+P{;-L&a@LRGl75;3{01CN4 zeml7WJ|T;#&aSpZX5rl2Ag`FKp`OyuHl5prR^B9RO8gF)c*zQXKA zcEhss$i-n6a$$unfGG?=P+6xuHzrypa-pe_BNyXGBywQ~UHWZ06gcNTdBn~m7l$W8 zE=FE7k?99`2b&MCT|CF{Q%;o!sZq%CAoamu-c)%o`PJll@xNyqJVxs~v1|i)a#KH8 zapuCgv-;tPI14d?gch9 zSFtBR$Vr!;h3X}h`e<^E$`_oY<%x<&$84Y2o7CaOMygM2G||9psVkZxI;_6;a&^2! zX%f+f-r@nB>Bs2Sxhmtr4BvIRZzflmSIAvZzUXArkE2#yZd8XB>mqgOnb1UAzV`C4 z<1qRH*?9CS^9<56S>JZ6te*{`={o*zC)b91oi?Ob#l$46B4C@#^bBPYCWaqS|MAHc zb;S1{>7}GX%`-TkcF*DbYEXx3**~3J8@xJ0dQqGZk}5OZWZlH|>e|I~dN+}tu`re> zi7T)bPQ1{VbOrX?v@5VbW4MCwx04H>=?a=upQYZxvH}ec%(}#QtxDnwOmP!eh@>kp zpQc@b`4z(z1V5cz8KsR`~g&PBGc^BJlJU!*A#xJ{T z7(W`Mp3EKWLUyAX<+Y_xuRfmt*8x#Wv9}*|BYDp@J;u$ zSQkGhnpkd;C&x%@Sh;!UDV@CVGQp7Kd z&)w&w{_7x1%Bpv9qkenVT=QPoZpYTFAfKt8zWAJ!Alq%je-0FF2)2WAi*=lsV6PXA z(XESqK8XI1{F}*L_v^B{0GOzYRjsPVSV>33BGl)LTJ(J9QV-W?<{=uh2F$nx>&<5Q1U_YH)8FIxSyC_b?NNEDQK4fPcM1m?p z$`O2xz!BKuBVGkeIs*G`+7Z~FF&sho+sS3mbOgIw79PcKS%J!5&>dJw{D3J+;_8s} z1Lo7TA27dS_<`W3lj{Q059oT`Bt>B>^)jQ!GR8mS)P(K4iA)hM7=&LEHR13_gV6Ds zF!IaEWuB`E+cni?g6x{Gov$nd{9R5-*mfPqO&l)?+mDEnu>J3YgsHG$lg`~J7v@UB z^4{+3lCWh*t?$On`hA>&u<0(23mGp6n{SAMu=&q}h^bIu(#YMAaIPS%+i^2|bt-3L z*N6>sP=TkCwV&nGgKbB3EIG&P!Q&@HJ=hMo+^JLUeHXXpxq2|Chr7HUJaR!QGt4O8 z!zl-Ez@nPTYvP6zgRb+sg^dk_}U~0OhlHk?g#21t6NUjI^Oo!d4k%U; zXr0x<1kdK-w?q*L{%i<5xKl!ZJGmm{ia>T%%xbU=FznK=ouMqSxYDnFom^Lb4`6|X zRRppNNd*)<^9P?2MIi92K^bt@gZ*@JUC0%I?4mdU!zu#1@@t1LiwcGbh|;PjlOj+(?-;~Sv3JyPsQ>? zye442A!-8VKMx|NJcIEjyCLCRO@LRwZBi33%s~ZC*8~nIR!zVjp2dWT*968-h?;;M zaG9^+H39!8ll$UaO+eHSf_+A8BvC3e%=NNr0_zQG0@N5p)=m9foN^JR1=8iEvX6Y z=hXxbR@4OGw?s_<{%lYO+$o{Iom>%eH37RSW=$aFC)i~IJ84<0fg6eayn`Wj)>J^j zGk@?YQ4;{a8kFJxZ|_}WG)=DaF!pel9KIBn>lQ81Ot&@pY$u&dvNoB%mqwoKtn4I@dZ?Z+*BYfJonQTd4H} zf>139BkUWSdjetj#j*k_Oq+TFmA37GDv-R4UHvJz00 zyaDhm^4X@IfCIu`Aj3TY&q8_vVL;v!&?N7;uhx13vxb+)N^aWRCh(|xH9P!?majNw zt0xc7?ms-+JKBHy;6eG3NR}wk2&6`5)1B)a*XRX~ciaTNQ2B}@lkEZuwC1C`Z0g0z zwDY_B@}KiS{><=upBnB~zx>JOmhg&p|M>nT9AIS?e)j`ur+AM)C*ut7xV_Z&beU*f zMcik@Cq8S#Yx@^Evs4Yw0re)q3^c(n5Dmk!SMXM6WgzT!g1 zgM~<*Q5eDa){@6E1zkRGaNkT3;)?JZnNB<>teTS)nq?& z`|Dykj3)a5;}}3q_5%j8(3u{3CgTSKS-mtFf4D`jtg*@X!w)(38Nd9r zrJAnEe!^pmdDLmLpTKRFyUBh6yItfS;TJT1Kl`G_AK0z%O~%iD^Gj*4De{AHj5eB# z9}HxnH5q^SQC)?l$@s&A;eEz052mZ>n(Qb2Egi+(Wc-2KEO(Rf2X?#2J;E<&{Na~1 z9!q!>`G26LY%)(0=i%p9E;Nb()8vUp7^{&cvkyPgpe$)E!9g~pIr#M!CWH0UEnMf9 z3-+Y9r{qjcj>zem?ee_ZGIi3`G1BjL4`IbfV+;Xd*&1|Yy{th;AhR^+C~RlU5;f>3 zFk6%j3yhCym6g0!SCSdSz(@lG)RGy*!fDVoicSr>w%d;eU4zX>gRVpMZx*RR*MZqC z&YilRJhwIMcCmqdw~LLG(_o_b3xhrrHDj37Z!l3ah}A-aiK1;ngNfQrLW7BdEkc8? zLyc}0sX^C)*)C3liG~V0&yVe5FJ_`)Vk>roiJIrfa`79Ys2RhoeuIgcK`fjG69vV5 z{N)mKG+b`X-ujN=nLS?$oW8k#|KN1_so}<*cMU(kG`xT8@4k9^dicii@_VNLUU|Rj z39p2HG3X~hmQ)`ao_Vx?ba-$7>|k#cYx%~DOH|gMdwG}|)8}`nW~z5R$Gz+Mliqcb zk*JB7X1X=mrcylIL{OO_wRIwb&4*8V*XO4j=%@v>it|v{)WHDbm~hwB!2km-Xg{br zX|f+%JT}=6C?C`GN>mVPz!8W_!8is`lktOrtX`UoAC$Xo#oLqK^?CK)c4QyA&$-n!d2Zk~ zw$kLef!$h2x~U>L*R+ds5Kns7Z@CWx$E_6X?t%7m!;U_?ORft0G~SXaoifiG-&W^O zde>jQzkhu1@c50r`}=2at{xpcIyl+`WmSWYLi^cxW`m9bv(-^U*TuAqbFXaZx|lHx zj5I(%Q!|5DIB79OMzL%SjX}HpXlM+A%}0Z-L-lVKsX^C)*`jP{40>)`JGF}q?7LlT zR5vx4sA(B5V4`LWv-%AtY6h`dXfRQQM%_24EIxySCX)w`H zpyxVn7ke=i4HH|j8%)%+jC1iDqNo|etbT)unn5g_1``Fv{9|>AS?~IfE)5@0y=&JJ zPP*5=E9@^bk3HkZ5~HhG4Laq+FT6B7buRDnQ$KR@>+Y|7+ti%(4fkjC|8iH{({(0A zvAsRnwDkw2U{m*Hc0o4>!8pcRn>L_eAZxKr8&FiEHmSg-!zL9}!wk`QDA)23(X@vP z+-A9(_HcpSE^?FofKG7^qp5)b#xa1J><0{Fp*7hLDo&d0$JUNb_5-TNG`kY}2_;~@ z@J;p;xXp4m*-v1%i`*l;b-5nlf#>xIj}+Zx{Gd~u>!8W_!8k@6O~wxfvU+JUepHh- z8NaPcn~WdSrA_t|ipzZAo9riWo8@k@pTKSxxkvZ~jXxBqk0t!V#t%Bhxel6)AB0-t(FdCXJW)KUfLDwiXH|W}KKN@rm zHXjYT4%NR|qy}9FX1h2IeL>G{4ZB@zVBhUxBjq%hsOcClV4`LWv-%AtY6h`dXfRQ< zO=vJtyGdv;QLsg5&~>QM%_24EIxySCX)w`HpyxVn7ke=i4HH|j8%)%6jC1iDqNo|e ztbT)unn5g_1``Fv{P3;+ba?9U`0&iPZNL4C>T2|=@7_*-lzYhjk1u_ZG+{#9)-{7= zgjZUK-BG$9`{Pye@jmy;%bZSDSqbr}Ch91~C+hT*7@y&U=JK((ASKr47UGU#ZuRp| zVt=-S5iIBTFUB4P-(FyVN%Ed03!b8u{BFn0nm@|Ev&aaO@C9GcbBQmk@|iSw6z#t1o+wgb z9bG^@zfa_6ngm;`P6AwWqlYPqR#;sxn#?uHR$l;Zp<>XRRpB&Ol#V(=P*LEU#G_#u z9E@1)(il_Zj^fg=%=ss=X;=n~q7UUFG%T0dqu?|w7Y3Ll?^&{-D#jtw$1T1|?ZdrwLOOt)|n+Hpy0BfJJnwLXU$K zrK{;w@lE25QRI%|(p1a&C$VX&1&qL-X{s%=N5N^TEetS8-m_%LGH9v= zGi&}RJ59Bv5hmdaeyQh@$W*XKO&&#~saEyqCP`?j4Q)B)>Qrn^9HpSCHjqy;&{P|{ zd`bu!I1=h81x>Y)eiDPG+R*v4j!UBw;E5s?*0C$qhDm^H(y)w}qG*NH?Mt;`8f~Fs z&@|OTt|%RK1k2Z^(~tm_fz@=Xd7@}Fod&K+Fq&#Tl9>LKWHp^8Oi{F&P9xhSTYUi* z(Wy$A4pNk^rc=c?iAPf{xb$H?mgbEjcNCYVTFyU-O;ask1pZ7@ZJ9j^PE&1RfJyS6 zB}0}$Q!SWT^GDffsx6H$319F_J(onLf;DRLC>l+*>a1asgr?eXQ%Jcw6fnBc>=? zVRidbZJ0(|s2DU&wU8@HM;*cPwdph@zz&^io+w&Pr-5q{jHX(TB&I(lSxu)2QxvVH z)5tc-Rx@gq{FHDtou*t-x|&WE-z450)~RNnYbzmXnlA80Q48yz{U)VP@sq%xZWb8$ zG0o6MV#ORqE@&kaS=c9;+ZPJ?H%-n)Xw4m^ru7WxpTsVBp`JJY)SwzXP*3J^ep~7eL zOj6A&Q)s1&jjVU@M7dsR<(dTBV&w2CUA1UCy^K*rx)^ZINzU!2>CiIX>E?`*Qp-pr z4YN+7Qm+bhY8k1zG;tK@;|xq7pJX6GAk+d%0wS&^jq{Tr5QwMQNf3zXUQJK@D4~t> zlOPbOCjm$h2q(iJ7!g$?mM9Jtz9$HTDS{5w@{k|^d7@mzsw~&ET*SG|U0vHhAkJ0b zj6%{C&+3Duq#ql!9dR< z35r^oCXOPJ*k#IW61&2QwDJxjt|pE1lh_rArxhTvE1pEF>52O%v~hkCyCU@@0Eu1U z_*5)R$w52`u|#pG@IA3BOrcdS_Cb6G@569o zy(RmY34xE2>{DWm!ctGlgq3-xg(ca?Oo=p4vQLpW%1g2j=bi+nUWO#3R;G!gNF@81 zBVdw!!Xd44S|Y9{jq{W26Nsl3AlWA#&Z_B&`zN$lgbBJe7!!$u5rryi zFfvUGRfFBHL6t`mxyl+;T$5lV`VTo$v#EiC@;xAoO=?OdKr?CTA3z}B9ZKqpNbAYIt-`#j}BH}J2^f*TYh@{ zFZUA>PrDzEczCe;B&BUz+W2ZQa|Nzy772 z_4O~U?;NactlfM4%l9_s>-W}g-&?)E|KO}mjEBPqGbxsbDA9^Sy+Xyk^}*qNrw4A% z{j-w?%Qu$ab^gNHk1XH#@C}Oo*ZXOYAHH?|n;Q@AoeJh^1JRVEomh0 zrbGVZureVzAMW7Z=MRq#j`!a>SRIG=qd)(F)q4kz4qrdG^V*O8hC8=!*~#A3Hrel7 z8a{+(TmImWr_-n8_(EaY;B*x^7^f%5|&kl}HPaf=@E9N?k@*sMKXNkweE^MoK>F zGD0w!0CSmy%1y_XDm1gl8fo_yS*nz5n)>B-S|2mj_+Ni27+kK^j*vY^Q zA)ph(szOWb>r*u;G_@0hg)i_q)ua^`z8J%#ALg+|E+-xfUx2R?z9`2eAPZk*HYXtq zUw~0G+OZf~TQq#}sYYy0NEW`r-l^b|i3r?kHJO`{g)iTmr{OE~?%fWM3Z;qMTr7M^ zAD)IUA(%{nMTLaQO~=Al=x3M1S4uWb&3qitIsv$#ZQ(17+7iAHv6F!rLLe7ouR_y? zv&+I)KCBpaMHiARSt{|v>U6kU5m?jKbs?#brjj5*hc0ALS%iu%qLWvl_2?oDD`K!z z0zMZirxM06`3j4kMJ}f)mP!C$C6!Q)NkEoL%4|+TmP!Dlcvr__B)VmlFrP}a%u-2c zyBFePA_BKsP3D$`r4rw0r>P_~;!CMS6S=uqDv_o-O(jAwnE;E>36-0Ug|E<@E~k=| zY?_++IG}X`a6#KrNf@;yd?8{d12cp`F2-JkrVWRtl6+V(?20aGsl*Sf(*sQKn0)dR))Fi5I32OAuHo`2WUW;)tuxllCzHscvHWY;#Gjg%AkigD zIcypck0p-qQ9of5~^w1mzqi4>@Spw#injGkxlcz+)nERU?V5rfcQmUB_jjcNt9V9gOi7? zUNVO)%k7r&wfXMOdpVp)-$Qsr3TDsA#7xne$jybFOR^+(t|U2PVKj-G1v{5ytzGO~ z^3NsbN@(0f*tx>Fj<+l9Tp3l#xe%$F9j|#H7X4ut$jM611!dJ<|4dP|ALN%cw_lW43Fxc05x^{y#KEKV@&`=PShW$ujc1-;i)B{zX**oC7LEL3YvxY;>t^iF0Z^$HLQF> zT3h+(ITb+KH%QP5%cNR9Cljd{b%Jm688O^Y=Vtl2hutLuw{7a za@0+Urpb$fHbvbqs`qW2?=q@s^5V+3McpuN)5?e7+_3T~ty_5}Z5HB6wXV6tUX8j1 z4kr>bolK-+)HO3`-@s9~#NwpFQP(hu#&FbS3~WjfF|0T z>HFdDz4ejdnLWS1clXTiyW7_98{T_ox=B6!?9;<{O_#){TiGMU-v05uz0)Ii2c6x_ z{zFg6&Fnw;)b1a6D&EX~*Y1=5|H%p@E6`z$fBZig-mk22`8_{ATICA-jzK>5u%8pG z6is?Xe?2h$5C8SR3Olv)Uk|Lbd_Azj&Sdl}O73^-*{PlU=KM<2*8?l;FieISLzbJ4 zHQK>1!;g7kg*Dn4!#L3jYqS@+oOtY@8}LmjVuc+zP>xAJcFe&DiTorwtSydn%FGcuR)3j>SlH%fc6* zO0>+vSJ(~}d@>P%TdgK@%fiB!Z|lg9Fu@7m6X|>ge;W+M)9tW#Yl9^QVE|*w9HaTXmc0hV{;EPVL}sBDi}_)5v9shN)hS|{V#maA+#YhZV!F=%SWN{IEJb&{P6w>$;GrPg6-4wL=#&s4PN7 z7a``?q4nq@3@c)=R03|Kms1I2n0$pr&mx!86iX$5uaZhA$0Q(2C1o}zAxkBIQM{{T zF%sRfRKlkcEwfY-diRC+n25lwR+G79VX4Gl{HLize;7c8(nM}9mP+K>KTRb#WX!IdemON+b(>JwQA9 zcnT4WZFLQoPa%tZJwTiJWD=2$Xk53JzJ$Fo&1_$a%Rhpk@1 zBFloW{$=B9^IgGrF$x?`Bz&tPoKjJHP9|oG)|By1v2!KK5j&SAakF6OlB`9X zOa8g!TnUYv2s>9efbn*Pohzd%ITs>zv*R@n#9~F*1#+^Ib3s}s05)cGlYUCK*qAZ# zBn&p@0*4a`M+d{>HG>_PP)|bP8&xG1Cl$U?Wtc=`jli{Ut>7C~Y>E+!6Ab%)sH}+Z zhku`zgxR5%&&`E>Urvdz@9X0qB8(<+vtZwsgB{}g^3M|~NYEdlMpcv*R@n#9~R<_j9soe(XAs)(LYlB2xHiZ~4j=J$!h51%8HH!e6$Snenx^kd`qpm(FA&#y|+$=ci z${7fWy7JGH-Qjqe(71^hRZG+j#{t^1us0!6H#=VQKrEJoqi#+%!N#rwX`KMrGCXxT z>ZU}~95+q5~4RbfIybLHVQ(XC4)D6>X-ylH?(z=yrc9Mm-Qq(oq+pAHx zz~Mw9Gd|~``hEELd*?awH|MYbEyN8b~|B9oN z*YDh&_|j+m>8}p_k)`3Kk>S@KJb1%Bz5m*q2e1F+efMJyXQ!*LhyQ!;{)2-rAAZ$+ z^6}J=c(lzHPku&W zRi#z;Asaih2Mg#E%afl`cyph=_@^sX`kIs`aw=eF_ELD#7ypEy>@o?Jn~t3mht}%k zDD3)IN;XZ+4z>ZU6M&uB2kOZ}=__*?-6R4#aYe*V26hG)$i>*J&~&*)A7{&l9c6Z6 zup>`!xxO5}7{jzU?3PWD%ZbOX905MBGxi-9<(LFySB}bTPC^#G0HbKMV=)rlvMWb? zD$z1KIS!k}f=?zQaI4j1ZdursBj2H>;Y)Y0sZg58&Bel(bg60h5`xJDSX4-;+;l8_ z`5vV|+@`+*laftSGam=EP5>@w()pp`D~zfVG5r-7MC@c>h7icb*sIXA;n47v4=aXU z(S^@*HC^a*NMH5zB52yWE+qBUbRl$;Ij~d`L{N&W>moXNMHhi^l7ijT2~udyIWbr& z0hjB`sf001VzKC1u~Z_hcbZCsU@`#~p%W@M9SdK+@mfnI zDcLkN^Kn4y1mJ?UrIIkJibyn-AYvy2GlW1c#$JV{4Tq+Zd{{B;iY{uY#1E^}15G7> zwyq0F5j2&AQ9E=YgUTXQbP+N{9a@hr!muI+OC{iPeL0mdhRIi0^el2YO|eu0_$sM{ za!dlUR8nSh60%eR7{$9f79-Iu+m-OCM9VCdgky+8d`v{(R;$U}vanR*zhR)MM1SKz zh0;WBE|yB<3kI4>gkUlO7NHX=HysOK{yKRrm84|T)GXctS|{V#maA+#YhZV!F=%SWN{IE@_1kl!XAyJ>Ek}ztAE@V(ygo-Xg%%`a&2+^a9 zFsz8d_EP4u{gg-+_z;42^zjrT7~5VME}udc`4D24b;HOcA{)!UhE4owp${QwAD>Ml z;<3aLK7=Sl!^D`ljcPLIQ26GM1XX-<$p7moGx5WDyu zb@thTT018XTfIacSr&YEJR4t|?~|WV*ni6{GIlP>TEw~JpG(e_(71`PbAY#xZz&5oTb5Q`OI7s$y<&IM_yF`LWxQ@X{*jEN^<@ExfFhZ6}$2gBnvgB_Sq zPeS3#VI>wP6}}v1m_%cZz!T5Mm&4c;BNitZz8n@RE0W8?zfViT>`=?+=E8|pPKj`0 z)yF?X7)|14!M-mCJH+?ppC>%ns}mYG5u<8Jtl^A9B?pok5UHCTuX!LAOTxaNlTEO( z>p)s30KPWpsb$;K{2>I%)9G+xw8fQ|5dGw56prq@yCXR2nw$D#h@Q*<2cH6mGX`cl z@JcQzo6HQ_H*nM~u{f!4)HO`vD~-T)RD1F>3a`3f8+A*KqpqBRkfT=XgiKfYmg6gJJZRIOb zH_WYly14Q(;3#KpdHB90wIO;O;$!u}0TjX(q;aWGIKrr7*jD|%3P1N->3jg`j!}n$v zv!DEuLh>sBPktBT@mHYZmlXcy`-f*zgIs=J_-H_X1+h~4pkSqD)cH3Qf9|IFu;UNk z9R8b|cW!)m>B)bdticl-cwz%38@To4W5dFymBSxf^1qMs>gnm>8^_Dp)AVyjIij_zNA%Y-MC$<&yU3BA z+ZrvcUp=P3lwsNknAp7}IU_b`HX@pNaQPR9k1v0$?DEs5k@$S4(d8=I3~<_h)+)EU zO<>uV40HIKnToaoCU%udROH(aMpIjt3;`_S@Jku0?SP71DghsQjy(_qZ#%-<4TiWq zY%Z^t4e{Q|SC042Phd`t9-bYZ9IuYUcN^w&2=4!#!QBaP*_9XucT}puyaOh^;$;~A z^^)HVN)o;uK(kAvMbPd&ieHu|?AzB6_rJ<<$LfRK2nM*PuxJs_iilqe2EQ|GEpK!h z{DPtQIjibey0EKd6!K)?M$_}_i2L8;CWOhToPi zLM)#;BAjmj+LhS&|HVrDj(jCz<;5=kb90co#l(QkYi9yLIm0jJK_OOb>_%#QIoM!U z#oAgh`%ey^Sbm{w_KRBi)6TNxKj&5sH+TPA4l$N*d@oeCb5*rnADsU*#~JH3b|*Nf zl3MXjwBqZ7)NKT(4vsI@=CN2~%?E$RYeLrib_Ceb{PDw)u*^4tu-+!GEOl5LB0+_$Z?TS%4BM|7U>w8W&2h!Tg*%nM zra84A@{Bz%H;uMK693fj^75@x5)W6W7i>ff|0o9+E0K;@ldPLvO?EKpdr98mhrfw9!J za^L4pFy7n4r#oHDhkdM?tC?DJ zxzxE@+{V0P3B=AUz>{mZXjm(^c{R4H^V+%Sx zvGDT2+!f)@k1f_?$;FN>%D91zua}SJt_XR4Y_T2-F?MVLkh5ZCz1$`0IJV%E5;Jc* zwm2_a?wVtZ^;oBMM7UxBvtBOSuFFc~#}@0cfMdrNTr;ewSyu;m*915S#^%^!J(hLs z*dl!BV#@Y)b+@Ub$sGplwsJYLIREdlI7blkV~h1z!SQBK+0M&#+tolH@?(qjSh(?x zn8u0XxjKBi8dyl9`LV@%EY{euh5zQ;nrykN)zRYjGY}i*CmDB#{MceW)?n<|LVv<; zF{v)rWdJ}b%8xD9W0A$2PGdr*sJd5Ig(^R`*oY+*JGPKV0F0J4RHv$=n=j+_+gXd6 zHezMOjx9t0T>5U5i=j&aI^@R|8?n@3z0(90u)d9QX>&Qa@?(pQSh#SfDjr*Gs4AtS z;T@iMSnQ1AE;7aZ*kU7ABJ9{A6@wULxw&+SpohHYy%7r$);I(Rk#VXswMztqw3r`T zY{VjjyI%R&Vnfv+9nJ2rbPFC^KqECOjFksFw#eilR$4AYE)({UA6sn1a)cdQAdJXP zRbg_OkdUF~ecg>%ps;E=pk$qZ4OOCabglDoNowbteZ393d%oF6fb+iYW~^PR(MRtR zOd+$W3Ylw%I&b!E#sY^OTjc6sJN!*`2zTvp=cU4CEP*Peg1_fu)m*NC`t#1B7mDth z8xEVX4yu$2`f8L#_*liz!f=qY=6Pl_7DE+h)d)cd-n{5?XkpmqhQnqohbpClTn4fl zEH`HQbB@eJLUL(WZ&Vm7p-QPBlj2fg^Rdby8M_}4GSj?N*o@^+rBo0(XmC}BWtXWo zgfcG`He*3lDHTFz(^_izXskbO4PeUG;l%Q1G(0YGQnjjeKzs^gHGPfCpcB@^I(#qgbXez|LP=+86njsbu8 z?fELia;+oG73-L-a`kp?mf~mfrHF+byXnAnLyH^L^|~&|QP$7D(Ao^z>0}?~;sL~O{{HnutEaiCDr)cThs(`ybIP3v8xXLS1YZ_mTR+)Gj%@$vEfoRKKzXwPb|gQT?hRuoEDU-zAgm;a=X8k zgNn5lb2|+R8DqJTbty;(_n22wHgCsDirsdQ$G5GXRK?VBST5r&U}{Z@iUkt8?jRE2 z8hE>06I~9{;eX5(6)PUrNljoO11wiKmjvwZ<-lV7!X2x4;bB`9D;-A`{yp&WQ-!6# zhpaI#NOodD!j3UgIj~LcsM6%BA^tC!0(W9T!a9inBCyXLRhL{f$bXQ7jMWHtzw(`j zopLeK(Gd^JS-b{OGTp%_G1egLBqN)Z7;3p7xm=L{Bv)cAOxRHd0*Opjl_-~r3B{cJ zd!L+i%erW%9v zp25^KJF2F+hPd8&9vSBCILzS|@pJrQCm&>943{*Zg zFNR}NHpHT+Qa0$j0T$w8RYr^BK~c&D;}4FHK88mmV`)_JS&h*J<7M-9d9HXdhJS02$J(h$JEY@1=b_9U#Z%3F2H@mPG0qCR)7{68cES6sEegqYOnEFgrVjZ^x=rjx9L<;|R zM&Yq0V>cwIOfegq$gMqAYV3{#057c&YwqQ0t>b*0&r0lo*E#W$gfvQS z3_oiYB35!85wBd{%v51_ZJ;Bc_&Kv0vBYCHCAfmPQ>aeawO$Xi0iVUfk6o4szl&+B zotY~2Iu6pqh}{<7*c$xjCKSdRk9UeHHm@qX>xA|HFj!-4$G2`8E2?Q`<#O(-7U2Bs zI;(18LB}pmY)rrX0?fC3ZN1|(-cLd7xm2zX?vg=@NIllgu1@&rNPV*4liiDBK#|v- zU1zXjA;w%#!$L+mSB2N5CspH%y8~lP2g*gyW?|ZWYkxY^i zCNfy}#JBzN*Z8XFrD7kbXy zKlvGm62|4ns+TTSm!^dU4 zUywdcA)%h=qfxn#aIbQ|cUbzu@{QX|x2_l_ zsO9X<3iTX!rH}|JpoN5chxUybk#>*Zfgc{SMk z*08a>)@ARDHaV7nqGPGG)@t3;^vt|f-E}1xk)q92DfMbMyFg;>Tfv3~t~yuO2Dtk% zjm*MhMaJ&M8Q`?@v94;ejyqCp0usS_l>)9xAnm)~08bY|y#(GI-O2@wejofAuY~(C zjcg_2`5n7Q!n8#Tn#!x<*JOF@=2|^G&TdU?tWsa7ygA-+wjL(w_6OAdGq)xh-XRYX zkQlEYBEKqN$kENM3H2;F-hME{r%&6S)J;?h_;3J zM!D&=8fujnhL0|P-&m`ZCLo5(AK-k}UI9^Wk$+!lE@q;tzxB?GkBre=DcFdm)<>&| zpdpq&-BvHEyJ)l0X7fqi`V*z)rw11r|P1q*9aQLK)hFT!C~ zrwXDg!sLF-B~_7nI{agGOvvu^o8ec4%Ds@Cp^AkILsdCxb}xdL_TgK>Bwra`TK;6o zB$J62h<&F2t=|9sBvDy1C$YxO@>h44h|oQ+oT)FK#J$vv5Zb0{AzdCIU^&At8H}-U z^g;eyd3cX0_#71RcONO?Ls9&DS@Zv0wCZ zlMo|jXvgJl(N&^0*F)69;Y`yf56|vDJli|kfBWD8y(YP-T1B4?;dX79Ew7fsEf|~V zmX`z1AP+X%2laONDp4;GhS_!b^7Umez+f8t>f!KNCt7IHsusFza7er6t3mZ-ILkqa zc4?H>4~Bm(*zx)>U%uUJ$9`~P$UHDFxnL~+dDi{k(6Ik~o|@FF;kR2o)P&)pB*mdF_F` z+x@sjT4AbZ!`W`TV%4hcb6rrnCu1{|@zxWoF*QnPyXs)>y5Kaet*uyS;ntSFkpzNM zxv*+(nv}0R@k--4+uBkuedE@azPtqxsj}&v7auvBX>D!AHV}3;jR+cHxj1^~VcOQ# zR_q1g)|T8U0valRg4Wzakr_zHEE%0qk$ONIx3*+5Qoiapv@ZbwGCR}SQZHuXCQyP2 z*`0nd`-)JR)|Pr7o1sd&Ki2-->)7E+QFBvdF5mks*kOGRsm!#t)RWk_wUvp%tg(Fi z`tm@Z|4wY0lDA{q2X8A7LTH=ii$a%((X_UtEAP zX_@cXw#x-g^Nw)dbJvj<1kc)b&vvYDa1Q`a%}iT$doCRu$~@+7pL(zx@A(UOky-1P zyRQY}e~}xre!m;;$qiz5R_h+{E}fNqe`tDb3~S3X-t;09k}zH}XRF@W(!t$JGB>ej z_ojC!p)MaDSZ#aYvJ+c4Ydz{65kreE7gd+P48ymKNyp;ql{U<@0NGLf9dseNYj|S8 zyby_37pJBcEJS`weqB#J0NxnGboa+@6x?IrCBMD~%QCp1NlWW%^&>F zVHHgOR=p37`?ul46QK3-J)-6&V3?xY`gIw8ew_T=VEVV}8F1E8s@S%=Uvw3afM*S+ zf4dWlFxfb}!|+xhKS9^wA&8oQX_SRNXymR$RHg4eN zf)TTI?_-Cqz;y$if5SUhk7VNxE(DD%*}aWj+9qD*N|?t0=d*e(GlpMwW55j%mAn}` zTQ3i{mS5zZEx(qF-V%UjoWJ2SpT&~qMY#1%rC>&^s+%jtXOhb-);L8st=uM<>zAyr z6rrgNX7!548z*VS&S&M8O>b6_6LJ!XOZy_LzF7DaYXg~++2_SpKQh|_2=lYu`7BmH z_;@Hd;H170A&jVxQU5Fs2t;x7*}{Al>mS^b2pDN?;NF>DI8BCz2hkUAsm9vCl?AS; z1Y>G`wmYB2Qm7JhitQ#h&b7sypY6`oW7NgUAn1bHv3r&JA}fP1;o~cR$j^4?^Vqgw z&f9dh>jo-LE-Cr)@#cqr^LZ?c@VQ6i6`^I;yLpy&wL)MCVaL_=4SS+ znnsro52@E=rFkr+@Y!w&F|_D%QFZytV9s{u>XmGKwreoc0%WertLEc7HaxLlUWn+k z-Sg8eScv3jyL0t=c4G`H7bJ64gk94$$j^4?^H_}Gvt6bmUZ9ju$F9M`m~V6D>iKPa zw!223p>e#i3~fI83*&X0dQ;7vpY6`ov)k;rschTs$?dSgxGpI3v)#FRXPa%LG)fe; zD-Ud61@z%BW_NPt>Q!xgw!6;GcGt=mj+)PQ{RG5%OD{kUf6btYg%v*B^^=j>Vy#?F zT?VATVIalw3A>wy1kF$tOqYNFRY-r?fQl6oKHgoY$GdCgTB!LPQD#`g?yQnxYYCt4 z%52Q=%9naB1yug4SM#;lOTtH?2`FfKs*1S`49NEWp0T}HrZ8BQ6W%q|EoyGrt~VX$ zmckuE_lr3?^_Dem<7RRYgRHz}eTk@t{Ko?3>g8(O3PKQ(dFqF&FA)*aL~&I=4tlfdj= zgbq%9>EOUp+e1P1Kr}utDd2@sy4)4I^hK~|zUl#K=ERvXvoqg~aoWZoIgZ3SC3QUGhc(5_A1;f z)ujmCeA35eBsq)URoBPBcc+#`{&Otz%~*al2C;HgQhC(+S}ej(Wnc8p)pOVQ)R(D* z7AfxK>yW*!17={Ec1yJxFEQa`-*tZMyIDR-Yd$CpQxI$Ks>jXeuJg^HedG)mKkX58pUmc}D*)hwy{x|GvHQZ1xBHmzklNo4>4?zpR_TY?!}nn!jwB zzigYo?BpPffHMPPW@!wFnE^2~AZ7-{%z&5~5HkZ}W17gj9STi8j3juQS z9oc|bHz3vxh;;*E-GDFyZx|382E>K|v0*@L7!VtFE(2o2fY>k~HVgh%E!ckZc6V?gW}5IY9MjsdY_KWC{0FM+Vb9bz%OJgL0287@8cEdnmxr4qi;`=6DEVtO$MKv4DK#7nUM_$GnL8U?(RuGjLG10lfmaE zgU{EECQJsOn+)#G6wT}ggc;am@VUw0bCbd6CWE^sdOo?y;PVY*P9}rTO$MKv3~uU5 z*J{cqHyPY@A`Fc&CzHYFE`z6=8|MJoAGY&9-isY zL7mxWEV<;)=rf93@_Z6!@hOKh`0Nk4?@SA=F1x#X?{%_wroT>+X=7{PMM^8oLP&vXb^duD&gC3lr)Mv+VI>duTJm)upI z8AUGn&d%`4@a)(`lK#^E(UI>ief!idl7AQ*PtV`Dxw7(^;oC-**G`U4&+gn_o}P>R z;d=+iXNPBR-*I6-AMv@TUtHRF*WK^>{PvCKf7fqY8ovGP!T#a#{+Esp_8#p&I6VJ9 zPM5#=(@V?W`01tL&n^wmyNhIw5!tJBUUc^R*|XKxCM6C3_0qtJ#xr;))(cdukwT=Zk$Iu!mC=4!t)qzf zAC`tS%69MME6028Umu+uJv=)+ITlSj$+x?i1k3`Q9^V(*=j<>bckhxfoLPZjQCsc8 zOgw_WpV7mgwa@sPH$rz^q=QSWa69Ts6tu{*Swr1ahTx%Y&tHg6IR?|zS?0lg|sNwIi2t<-3f`9NtMov&NAM- zB`x}^&`vCt@4{v2X+>*fUMLpbWi+JxLRy@!ny3_uB$nJblPvK9oE{%bi(t+U11h9N zFmQ-JEW1%2!Qap5;j;t>W|ZhuNQ;0=;uK47j75-j^Lbz_EzMZ}%y%UQSP~kEBa-Ah)UMN;)2^ms;q0U-cq|V|@vc#;;DsXyytj+?Q9cipkXMur3 zLSy9;Ltb^$h!GFFdhd?p+D_Jks%lAto-w2=Xfm5 z<)i|wnchQUbygU^!$1pZF$@>XlI6LKQgF4Bdsr+7gwe(D3TZLWin*~wmlBG^jnrOX zR%c~nFJ@4oc&ZyzAQc!DgNpQR26a(&mR}dCo*b}>g$SkKY9((osHQqA$lPd9QVUh; ztaMnZStMVI;#|h8sCm|6tsnU7-V0G4d zOP%E(opNHaIx9R&Rj#vy%nQZpEFnY6FVtDMSsD zNNB8FqCA4XpV7l-brzUW(p{m>0$dWOSiQtp1X(wq2gc*T#nf3F@_JNk$Fek+lM1wE zdJl=!Sz-JR11;29VYpzHEYD?>f~%F>!(urgj4p;(sIvmCm>a8{QbLiqk=hH)(qc9? zij0NA-E)Ux$VKvsfdo>4(M;cFP#0Ba1!S!T)mmp|14{`|F{q|GD-7LeP*Mw3>a28F z5x8PdGBz@(LY=jtE>Q3#X{@utU?MY%uNxOsXKncRCJS|zkcj!SIxEn5vuAbI#;Qb` z!gZRB>Gjn@o#iP+DprmOnHP%HSwe=C|8Mw(i;etYjF_faMrAAlq?^wp`?1&YbuTkt zNBa8fOT+EfVMEl}=;ILFy=)%#d3}Ixxf$56`CAOXxHL5LNK{&Hon&MXM4en7(5roO zA%p#^?xF_#rLEASh-S*(dYULrV#Fd+9y`WnqLoF2g4i>ezZ-v4g>igmWEHY4I1pF^Ch7PZy%F~+xUKu7}wuf8X7so)*2tkNldSc z#{>EPwk76X2!Skeg~~54t_b4I z8d@~Odo{Qokb5w`Ayp>w!r-DoMnX!aMFaa~DYtw${WAtx&Q30m2p0`zu8!DEiv}}j zh$1-u6MDhh$K>JS62QzPv7HtThC`wZE&wpOAnD@qKycAO=0S!@(^mL+q*gTKgaQYr zEko^r;G!W=cNiWm8p2?~;BXy~(FvY5Vh;z$>oBqy7LENOt<%^aC`3LsrKAWi8IJv3 zg#nHIlIdye7XpzP$NoU&MaHo|CVv|HW2*p%b=k`oW4{o2VQ}mhBIIH;_Ro3m8Aox( zAj{dwzKh3W`4QM|%I4{Cz5jM;U|ydf z4GxH(SI-2Ps0C*ai-*ZPH<8J^GbI#>8>zj( z?4n&ZHj0dev=~|zg|rxGL~y=#S#itUp{un~NQ=^yD5OOp5!qQ<40L|ESz3%OjzU_D zO%9gtq&ZPZi$dmwVrfyxkn#&@5xkEO;|fb|oJp2=0ZxyPrA07jN7N~#MKExPKPL$w=;5=p2xgS%R7i_}OX3tuZ;VBdb@O>(JPuq;TJ%SHp?-JqfTg*dRG>A}dq^xT zhVeTLw2&6VaKS8Dp35i&S1Y-P#X@@+T@0^~76YxA8%uO4p-9|F?FD9OF&i62#=-{; zo;zf|xZ}5%$H(d{z}b<;3UwA3I3zSy zE>RxA-_PjbvpNgRDCw?HX8|rre5_t#EP||?&jaIe;9}}5@Tmh!b2+I%Yo_;*Se+Hd z?=a9pS`5Plvt)TLqZC}NMYM4 zilIWCm6HmLia~{;<)U51pf0M;3gWgJRBN4;4J@!K2Gvw&g`pb_N@}4>os|wN0#^)5 z#zqEJsI$VG8ihJ5(1;1~wUdIZ5?`M@&>>H1RO&1t^Xtj#EFnY6FVtD!J%)On#hJuB zSe;ei^!Qkv1vop>SfS1W1Bdv-$|cGp`1=_>d{$?H8718n>MXz|af;PTj75-j^Ht-( z#nf5;Q~Q>FR!%C=SXwmn9uiB7fxg2)3w2f)E|?`NoH9zm)k^MRvCtky7sD&mS%Fr} zja5!5p-9|F?FD9OF&i62#zLLtxkE8jsIziXfl)E2Ftp_Nia}jeofX7wHK^7)D;ro~ zRSc@B&I&^}8kE#Rl{zaORs^mXl#GoGs!(TzH#G`%R-h3R;%nDfXFYkKL!Q*A)LBC2 zg9@v&gbXSF$panWXP(Rh7$NHPzO>;L*vsY>+Wgptq_9T;=`eOj9YwEBhrxpR;X9QXo#1IB_Hgh$<}k7t7`?deDMNkW5=L=71EEOUNZqpf zT}4N2^`($-fQkz$bb_aixMlTYvGL@Aj?=?8j#rqiX5|I`-m0sKL+{+n1FFs{@NIe_ z5Ob(dc{{*&?&IsK^v=C+r8sf$b$V&F_%HGwF$P&q{Jjmuz2NO*@^JC@4`wEb?eu#q!yy|Q{PhDS7bIOg9ti&4 zip+xylcue3ewt_-@U#(o zI5=&Ek;Sm+N7^B+yD-YelAWNBTs1N;8Qw5<6$W&}D4Cwdej$)G#j!t7d69AKkIA3L zet#g~#KEy&4h)L1Ux>UgIQ9z>axohF&07l5)o>JN46>Y^TpkgQ{bsHX7o@S@3>sV! zhjBtLc>9<H1-RD$c$rupzmg7Oyhqxrt2Tl3y4;@TG&Z{n_6Aldl{+aF)ER{-@Y&@;5)rM>O5x@!{Fw z{?Xo}{iBBmcWw`U)Ok9p|DVaI@;5!I{4F1~aLGr72lv2;gTb!tq=X_$BefTj-O0$t z*3R*p4<4MHx`p}gmxfQij(?pY<=&e)5bV-UPAV{(={=B7H;S@~pnTo^{s85lOn&wJ zwHJrur4~8?(MIeMu*;nxrk+ax#sBY?h8J7J8Wq8=ttHbqxA#ulO7212X_EuOI6pe1 z>|=Q*O1RZPCIFgfz4YvUspJT%fucJ{(MJVv2i1fkej~L9z}-0+8~MOb-59t9n#-h8 zns~4HELBL6Q$Q}t?oqP~e&B5tEjKj6-+5!;He3WVD);*rTZrgyix1!zmROAA%i^rp5DG<(spcSr|qA^AVu6L|OjLw~09w7S- zI4m60-rsT$YStu6DOGLk(GSFet(0;rMNBVqMSDg-pV!mdB%9+kuMga#4)shQpbVTD013WQSF z^@lU8%C0}56J4-fe@g5Tusv5wjEt$U>ra?RMetCsf_BtHE4c^3c1)QZPz!}!zZ}bX zC9s{mCKDYr(R%6G1}7O8X`txNIvBY4%N2#KfR>}M>j%rBB8W2z zwr?*picKuY?N@>A`YUszB`ECr%cG43!RsKS4z+sG9v0^J3&D2%HPK;&F6{cjcroNS zW8nGOtV84zTUd3Z=QfrJEk* zIPJ448`2BpJ|+*8mF9RR1h25`hl7mTW!rbcArO1nJV>_dXR{%ggkp$uM|R3%TX;Y%;Cfg* zGM25-EC^OhXo;{!#Z{w(rWa^^Odeb{v?Mbv1~1*N)+1+1-v}>bYJ-fL@la%yS;SBabni$_+8oCynpRgau{h?dBi(hIaokAG$r;3SidwSM&cLxr3v$>2-X12C zrJ76tuzqPm>!oM?(u5X?UUX+&M!ERQ9aIyF_>I&a0C(qPYy_du3;<6l>t>(B#CG!) zP64?nyGPBM0kzrDG8FoymElJ3;uVW&3+W|{{jtRd@QcCvrB&%+i>S~q1p}u0Yu52B zGm1?t$n95w^-C*rBa0~XOUt8;2EpqfV-RZfqCG6k?-zpgOKYM-YxowwB?ZQdA;(1s zo}bM+L_V>F#hCpHuzqP#VhF!4L`E2+A#}tz%_&Q;D7#;Tju4s5jvS&;nujN=bZ~Rp zXH_<&7s!1~9w;l#@k|I_Vb>1_8MVu{?}S4j_Of}9Y}e0bLof@w{xFP+NKT1t2am`F zU>A=^$5J4i2SF?B`q3Dp0@?l@GYGs+E)S6H`pH}fVqw=GMv0E>l*hL4fLg%yuy|xF zTcKGHtd`IcVU3EbMhQ(X(E6A>xN2xgW?Bqh+OVrf&Xm3pUdGf0FJnUR>X9=Yq$P6Z z90IWtISrc!S&5v64Z*BO&ZKWwkDLj)0IWpLl*gm1M9!24L90j3WQ>-`nK1~wO61JB zJitoi%()Q6dgRpYyL#l*)B>&&ITIF-tP(jB76hvvIrR`Nku#+iXqCvBF?n#6$eA%A zc!dUw`l*rfSEVVT0AdZ6jM^h%4VH`=K`8V~Q|?hYtpAyl3XEoY4}@*0%=ie(*LlA* zKD}A~sx+Y!5UgLC5_<%!Uz!pl@eBRZgn3j1Yk+2y0-=@MgJAvAd~^h(&@YvDLM#1J zO(pRw@stHB>Mrsd$yK^!&f>3A%fS*Lk_QPy9U*Qyx zi?VyvtQk<79W6tlUs@S%^e$epn6{8!!q^{Md;q@~tY2D{9=3=I{ZcSsy1!-}&oZOf z#Dd&@6h_Lcg>;+Gr5G4l)LzRxjGa!u)}_Z z>k#?G78YanE5Q1tMTsH&z7QExN2xgW?Bqh+OTtat4NyCH^R%9+TdkO2wpvMrh~LZ&YVLa zRwAci^B^mc)370!^~jm@?dp*;As2v^$eHqZbd|`N@*rsS$eE1M5;-#lfmex~IhO}m ziJUnXf>@87x_wuVoSIs|RU&7?;*nJ%XTpMD)gz}Kq9t;s^a8CKkCGth!Brw>HWPwZ zXt1cC3@rCcQ$hj68Y~&LN5UE`88w1X=$EG4qjFgPGba@o&Ga4!+fteF5tOg@)-O#*M&cLxr3v$>2-X12CLt(TtlOA}fodeNPA1aa|~JE$fU@f)c<0PfDo*a*V6crV2K;-hZ$IYiv|9^T(` zpNBs>IeK_@cyhd|`2^r%%pNc6g%l<3?S&Kv8-0p3Gb)_21dFo!MPR*<+U&>*zPU8; zN;K1E8W_3fb2eFkg?K&sA0+P&81zb9XaR-RaA~a309bRk%qZ{|QSr^uOY;fz#h5+#=FpeSjI@5f zgE;u={r%&6hsSU1-QPcZb9MSjrf@4v$L;eSL_4)W?P2i%Ki4q}f_1)wXdhzKCjMOr zxD4?`qmRjhyW1lfG}^zmG~hSNgSaZ-Ii=w@=n=8#tb@a&`fM4M5(q*%Z$vbYFe(iH z`~?CP_0`4Wf#ILbAoCz&toc~lsHyo_N-g>-`dG%&;$s;Lf>raebcni-<@5rr=wpVd z#m5X2f>-mgq(4*hv4mKF6@4t_Xz{U>13{|!STaK0$1-w(RrIl(r^Ux|9t7<#Ee-gG z7rmeAwv1EOYAMucS^?G0;1PYMc?JZh9x`-huZIjxEs!c9BVqBNifLIdn~S&z*7+`< ztV>pAFlA~o0n$Y45#V3kOfS|!(a(4J4Wf@;!zwB*e9TFW)E>Z2lMoQXFDwoC_j8g9 zZO)iJN#@Ww-2YHR3#pfUvtd+ZG+%cOx;pE)@T@3Og2#1lBlf~?w@3h2{f{pV_{W6w zMLf6YFOnIeABw#SLl6FSu!xW?XRQdYVkcZld*qGdE4fMWTPh?P3Y&Y&WW% z(Jepos{)q*TZGl){CvNx(8$hrvHe4hD*W^!d;)zjW)B|!gq!*Ca;+$mSN_$~z^cSd ze<~6o zQ(O!ePT4G`P#yu@&*%~VKpWyHCDe3#NQ(oFif)WcnoA%r!sq-9zHhxgrd zU~u}y^(>?om^~~W9xj3LEC?3eHNzoBJ>t+t=mlCIlLv=8Q*0&#kM5eoAS$pqrQtm= zA{JmB93BD@u>dRjSjy4jV<`uMRP(W9gu0Jq zwTYkL5fF8tqJZKhRF}IW(te2SVAn2AieWT9d!Q0rBOn@}edIY%2%*I6;p`EFeck~+EYsv{l{6=aI0N0FW zYy{yKSZ6AEn$9ggZZ{=VB76&}mmF`M(@~Mwv@?~kjth^QO&Oht-bU<&$2-w{WF$WA zOz9`}+@j-tlO__u8z{Zhc<-8wi9{~Asg4Q-H`Qb!HgnU2)(g(uG@(VJe}i?V)>g9} zXtu9mepTQSV2iMNoOl~olojq?X=kc9)aWy~xm4j3=!-FX@VGNon;B`HcBaf=>Ar}0 zb*6O1B)}Ko^r&%Xsw61X_%~T+sx;E5aNJ!gvkB~lcs+33nX1f-v`srxW~>-4T&!X- zB~y0f5zzgN9x?7r6{LikZVzcu^ik1`aY=It?o zm^~~W9xj3LEC?3eHNzoBJ>m^8p%-X+nFm3mg}*|TzI_2 z%IHM&HexS4uD|k;k@$2!ru!Fe(eW-z6N%srlwNAQA4|qWA{X3Lr_q9&YBCX#){#>#VQt4GG#{|0o~8&5##+>K}x9U_K+4u9~Ipgmo%3^UWC!E{{Yc-{B=Ne`o)_oNG&jXSUfzuAH%aCSad%IhZyyUw^oE+p!G3%aCkq)W@!sG(0i^l`Q`!O;Pf<_BH<6O9S zOUVRE6Rn4UV{0}p5}oeHQr=M?c!!k}iujGx9srJ%W^4q3?#E`?3k7(Sl~9TBEu>y@ zydO(PMPk$aSi(9kJlA`{+aX(AE4fznHj_hZSJ zNaTW>Mv=z_XRfEoMC>M7FF144WLzXVt<`3$ClAl=KRnwz+JF1tL7vOme3VB(^)q^e zxK=Aj*;lI-1{xK|!UzQ%zj#=L)q}>hT2WRA`=2chtdvRsq`993AjTp(?&kAoUxOES zGqg%X2$gO%`8cD3S-@goqkb3Q^nmeJvm_{jeCzit4Iex`c<|`_Tk?AcU*3Osbhi98 z_bK-9^x?7d;N=&Fn}=`hzj1JS=k~KpE8n?%iq`?_YPk_`;YXV25kTU diff --git a/qa/L0_model_config/autofill_noplatform_success/tensorflow_savedmodel/no_config_no_batch/config.pbtxt b/qa/L0_model_config/autofill_noplatform_success/tensorflow_savedmodel/no_config_no_batch/config.pbtxt deleted file mode 100644 index 5913902a76..0000000000 --- a/qa/L0_model_config/autofill_noplatform_success/tensorflow_savedmodel/no_config_no_batch/config.pbtxt +++ /dev/null @@ -1,5 +0,0 @@ -instance_group [ - { - kind: KIND_CPU - } -] diff --git a/qa/L0_model_config/autofill_noplatform_success/tensorflow_savedmodel/no_config_no_batch/expected b/qa/L0_model_config/autofill_noplatform_success/tensorflow_savedmodel/no_config_no_batch/expected deleted file mode 100644 index 165300aa9b..0000000000 --- a/qa/L0_model_config/autofill_noplatform_success/tensorflow_savedmodel/no_config_no_batch/expected +++ /dev/null @@ -1,41 +0,0 @@ -name: "no_config_no_batch" -platform: "tensorflow_savedmodel" -version_policy { - latest { - num_versions: 1 - } -} -input { - name: "input" - data_type: TYPE_FP32 - dims: 1 - dims: 256 - dims: 256 - dims: 256 - dims: 1 -} -output { - name: "output" - data_type: TYPE_FP32 - dims: 1 - dims: 256 - dims: 256 - dims: 256 - dims: 14 -} -instance_group { - name: "no_config_no_batch_0" - count: 2 - kind: KIND_CPU -} -default_model_filename: "model.savedmodel" -optimization { - input_pinned_memory { - enable: true - } - output_pinned_memory { - enable: true - } -} -backend: "tensorflow" -runtime: "" diff --git a/qa/L0_model_config/autofill_noplatform_success/tensorflow_savedmodel/no_name_platform/1/model.savedmodel/saved_model.pb b/qa/L0_model_config/autofill_noplatform_success/tensorflow_savedmodel/no_name_platform/1/model.savedmodel/saved_model.pb deleted file mode 100644 index a76abafbf76990f47d12e0697e119113c0940adb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1407 zcmb_b&2G~`5Uw5DaVKq(PC-IjPCgK1iPX-~w~}z^C9SY?;SweGmPS~1WUndVL3kUU zg)7g1kYLw;)FGf0;mhpo`kU{YKV8Ca0AFPMEQ15Biy%M^qz{JV3A^EzaQl&4;|%zv z!ZvH_^r1UC>YhrnqN%Mz9osMkWxPlk9tyDHCca1babqZxlzGMx!JmMP(VsA(5XSz!3DyfJSV^HVB}uqIJfEm=0)h#tO&a3}qA;L+3hN`1Cdo1DcRt z{hJyH#l|rdhniGPZx?Hdg&~R~KaoTM+-&*40-WR(Fw~SL@2Ls)&>jt~7l}U_!E%sA z@1poF8sH}l)^O~-nz~o7=aC8+GLK!a+l+RT@vqo_(*tk#2u&s^=7==2ZMwGE7QK5&t|GQOdx@e&&0tp3wf8Sy zB?5d<#}_W|Nj}9yBw<21v_fR}-jMR~6mR(m%a*n`TSa15Bs`n{PvSzioU*H#mycP! zNTkSdZ^2cMG}sPm<5tkRpZk{s4-8p9GrvpF6RWd|-p&Jhv&ce*UnQ_WE4Sns^crj9 zSp63HeHChijavx&4+tDVyQYhrnqN%Mz9osMkWxPlk9tyDHCca1babqZxlzGMx!JmMP(VsA(5XSz!3DyfJSV^HVB}uqIJfEm=0)h#tO&a3}qA;L+3hN`1Cdo1DcRt z{hJyH#l|rdhniGPZx?Hdg&~R~KaoTM+-&*40-WR(Fw~SL@2Ls)&>jt~7l}U_!E%sA z@1poF8sH}l)^O~-nz~o7=aC8+GLK!a+l+RT@vqo_(*tk#2u&s^=7==2ZMwGE7QK5&t|GQOdx@e&&0tp3wf8Sy zB?5d<#}_W|Nj}9yBw<21v_fR}-jMR~6mR(m%a*n`TSa15Bs`n{PvSzioU*H#mycP! zNTkSdZ^2cMG}sPm<5tkRpZk{s4-8p9GrvpF6RWd|-p&Jhv&ce*UnQ_WE4Sns^crj9 zSp63HeHChijavx&4+tDVyQ> $CLIENT_LOG - -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 -fi - -set +e -python noautofill_test.py >> $CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Python NoAutoFill Test Failed\n***" - RET=1 -fi -set -e - -kill $SERVER_PID -wait $SERVER_PID - for TRIAL in $TRIALS; do # Run all tests that require no autofill but that add the platform to # the model config before running the test diff --git a/qa/L0_multi_server/test.sh b/qa/L0_multi_server/test.sh index cd5ff3d407..d81041e8cb 100755 --- a/qa/L0_multi_server/test.sh +++ b/qa/L0_multi_server/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2020-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -54,7 +54,7 @@ RET=0 MULTI_SERVER=multi_server CLIENT_LOG=$MULTI_SERVER MULTI_SERVER=./$MULTI_SERVER -BACKENDS=(graphdef onnx plan) +BACKENDS=(onnx plan) THREAD_COUNT=32 LOOPS=32 diff --git a/qa/L0_nullchar_string/nullchar_string_client.py b/qa/L0_nullchar_string/nullchar_string_client.py index 1ab76bcf03..812e17ce95 100755 --- a/qa/L0_nullchar_string/nullchar_string_client.py +++ b/qa/L0_nullchar_string/nullchar_string_client.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2019-2020, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -89,9 +89,17 @@ # Send inference request to the inference server. Get results for # output tensor. + input_name = "INPUT0" + output_name = "OUTPUT0" + + # If using libtorch model, set input and output name to "INPUT__0" and "OUTPUT__0" + if "libtorch" in FLAGS.model_name: + input_name = "INPUT__0" + output_name = "OUTPUT__0" + inputs = [ client_util.InferInput( - "INPUT0", input0_data.shape, np_to_triton_dtype(np.object_) + input_name, input0_data.shape, np_to_triton_dtype(np.object_) ) ] inputs[0].set_data_from_numpy(input0_data) @@ -100,7 +108,7 @@ # We expect there to be 1 result (with batch-size 1). Compare the input # and output tensor calculated by the model. They must be the same. - output0_data = results.as_numpy("OUTPUT0") + output0_data = results.as_numpy(output_name) print(input0_data, "?=?", output0_data) assert np.equal(input0_data.astype(np.bytes_), output0_data).all() diff --git a/qa/L0_nullchar_string/test.sh b/qa/L0_nullchar_string/test.sh index bded41dc92..5dced6c5d0 100755 --- a/qa/L0_nullchar_string/test.sh +++ b/qa/L0_nullchar_string/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2019-2020, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -42,7 +42,7 @@ export CUDA_VISIBLE_DEVICES=0 CLIENT_LOG="./client.log" DATADIR=/data/inferenceserver/${REPO_VERSION}/qa_identity_model_repository -MODELS="graphdef_nobatch_zero_1_object savedmodel_nobatch_zero_1_object" +MODELS="python_string libtorch_nobatch_zero_1_object" NULLCHAR_CLIENT_PY=nullchar_string_client.py SERVER=/opt/tritonserver/bin/tritonserver @@ -53,9 +53,15 @@ source ../common/util.sh rm -f $CLIENT_LOG $SERVER_LOG models mkdir -p models -for MODEL in $MODELS; do - cp -r $DATADIR/$MODEL models/. -done + +# Copy the python model +mkdir -p models/python_string/1/ +cp -fr ../python_models/string/model.py models/python_string/1/ +cp ../python_models/string/config.pbtxt models/python_string +sed -i 's/name: "string"/name: "python_string"/' models/python_string/config.pbtxt + +# Copy the libtorch model +cp -r $DATADIR/libtorch_nobatch_zero_1_object models/. run_server if [ "$SERVER_PID" == "0" ]; then diff --git a/qa/L0_output_name/output_name_test.py b/qa/L0_output_name/output_name_test.py index 905174640c..8affb2e1e1 100755 --- a/qa/L0_output_name/output_name_test.py +++ b/qa/L0_output_name/output_name_test.py @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2019-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -36,7 +36,7 @@ import grpc -_trials = ("graphdef", "libtorch", "onnx", "plan", "savedmodel") +_trials = ("libtorch", "onnx", "plan") class OutputNameValidationTest(tu.TestResultCollector): diff --git a/qa/L0_perf_deeprecommender/run_test.sh b/qa/L0_perf_deeprecommender/run_test.sh index 75fd68704d..434803e7a9 100755 --- a/qa/L0_perf_deeprecommender/run_test.sh +++ b/qa/L0_perf_deeprecommender/run_test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -28,13 +28,12 @@ STATIC_BATCH_SIZES=${STATIC_BATCH_SIZES:=1} DYNAMIC_BATCH_SIZES=${DYNAMIC_BATCH_SIZES:=1} INSTANCE_COUNTS=${INSTANCE_COUNTS:=1} -TF_VERSION=${TF_VERSION:=2} PERF_CLIENT=../clients/perf_client REPORTER=../common/reporter.py SERVER=/opt/tritonserver/bin/tritonserver -SERVER_ARGS="--model-repository=`pwd`/models --backend-config=tensorflow,version=${TF_VERSION}" +SERVER_ARGS="--model-repository=`pwd`/models" source ../common/util.sh # Select the single GPU that will be available to the inference diff --git a/qa/L0_perf_nomodel/run_test.sh b/qa/L0_perf_nomodel/run_test.sh index ce3350d97b..fcfdf16e0e 100755 --- a/qa/L0_perf_nomodel/run_test.sh +++ b/qa/L0_perf_nomodel/run_test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -27,7 +27,7 @@ REPO_VERSION=$1 -BACKENDS=${BACKENDS:="plan custom graphdef savedmodel onnx libtorch python"} +BACKENDS=${BACKENDS:="plan custom onnx libtorch python"} STATIC_BATCH_SIZES=${STATIC_BATCH_SIZES:=1} DYNAMIC_BATCH_SIZES=${DYNAMIC_BATCH_SIZES:=1} INSTANCE_COUNTS=${INSTANCE_COUNTS:=1} @@ -50,8 +50,7 @@ SERVER=${TRITON_DIR}/bin/tritonserver BACKEND_DIR=${TRITON_DIR}/backends MODEL_REPO="${PWD}/models" PERF_CLIENT=../clients/perf_client -TF_VERSION=${TF_VERSION:=2} -SERVER_ARGS="--model-repository=${MODEL_REPO} --backend-directory=${BACKEND_DIR} --backend-config=tensorflow,version=${TF_VERSION}" +SERVER_ARGS="--model-repository=${MODEL_REPO} --backend-directory=${BACKEND_DIR}" source ../common/util.sh # DATADIR is already set in environment variable for aarch64 diff --git a/qa/L0_perf_nomodel/test.sh b/qa/L0_perf_nomodel/test.sh index a213d24e9d..c9a4ece9aa 100755 --- a/qa/L0_perf_nomodel/test.sh +++ b/qa/L0_perf_nomodel/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -168,7 +168,7 @@ TEST_CONCURRENCY+=( 16 16 16) -TEST_BACKENDS=${BACKENDS:="plan custom graphdef savedmodel onnx libtorch python"} +TEST_BACKENDS=${BACKENDS:="plan custom onnx libtorch python"} mkdir -p ${REPO_VERSION} diff --git a/qa/L0_perf_pyclients/test.sh b/qa/L0_perf_pyclients/test.sh index 9b7e405977..a0b4def392 100755 --- a/qa/L0_perf_pyclients/test.sh +++ b/qa/L0_perf_pyclients/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2021-2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -43,10 +43,8 @@ REPORTER=../common/reporter.py CLIENT_LOG="./simple_perf_client.log" SIMPLE_PERF_CLIENT=simple_perf_client.py -TF_VERSION=${TF_VERSION:=2} - SERVER=/opt/tritonserver/bin/tritonserver -SERVER_ARGS="--model-repository=`pwd`/custom_models --backend-config=tensorflow,version=${TF_VERSION}" +SERVER_ARGS="--model-repository=`pwd`/custom_models" source ../common/util.sh # Select the single GPU that will be available to the inference diff --git a/qa/L0_perf_resnet/run_test.sh b/qa/L0_perf_resnet/run_test.sh index 579d00c0e5..3e7b048c40 100755 --- a/qa/L0_perf_resnet/run_test.sh +++ b/qa/L0_perf_resnet/run_test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -28,7 +28,6 @@ STATIC_BATCH=${STATIC_BATCH:=1} INSTANCE_CNT=${INSTANCE_CNT:=1} BACKEND_CONFIG=${BACKEND_CONFIG:=""} -TF_VERSION=${TF_VERSION:=2} REPORTER=../common/reporter.py @@ -36,7 +35,7 @@ TRITON_DIR=${TRITON_DIR:="/opt/tritonserver"} SERVER=${TRITON_DIR}/bin/tritonserver BACKEND_DIR=${TRITON_DIR}/backends MODEL_REPO="${PWD}/models" -SERVER_ARGS="--model-repository=${MODEL_REPO} --backend-directory=${BACKEND_DIR} ${BACKEND_CONFIG} --backend-config=tensorflow,version=${TF_VERSION}" +SERVER_ARGS="--model-repository=${MODEL_REPO} --backend-directory=${BACKEND_DIR} ${BACKEND_CONFIG}" source ../common/util.sh # Select the single GPU that will be available to the inference diff --git a/qa/L0_perf_resnet/test.sh b/qa/L0_perf_resnet/test.sh index 35d5b174be..11538db4ce 100755 --- a/qa/L0_perf_resnet/test.sh +++ b/qa/L0_perf_resnet/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -234,6 +234,5 @@ if [ "$ARCH" == "x86_64" ]; then INSTANCE_CNT=${INSTANCE_CNT} \ CONCURRENCY=${CONCURRENCY} \ ARCH=${ARCH} \ - BACKEND_CONFIG=" --backend-config=tensorflow,version=2" \ bash -x run_test.sh fi diff --git a/qa/L0_response_cache/ensemble_cache_test.py b/qa/L0_response_cache/ensemble_cache_test.py index 96b959cc8e..36821d07e1 100755 --- a/qa/L0_response_cache/ensemble_cache_test.py +++ b/qa/L0_response_cache/ensemble_cache_test.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -48,8 +48,8 @@ def setUp(self): self.triton_client = grpcclient.InferenceServerClient( "localhost:8001", verbose=True ) - self.ensemble_model = "simple_graphdef_float32_float32_float32" - self.composing_model = "graphdef_float32_float32_float32" + self.ensemble_model = "simple_onnx_float32_float32_float32" + self.composing_model = "onnx_float32_float32_float32" self.model_directory = os.path.join(os.getcwd(), "models", "ensemble_models") self.ensemble_config_file = os.path.join( self.model_directory, self.ensemble_model, "config.pbtxt" @@ -79,6 +79,15 @@ def _update_config(self, config_file, config_pattern, config_to_add): config_data += config_to_add f.write(config_data) + def _add_instance_group_cpu(self, config_file): + # Utility function to add instance group of kind CPU to the config file + with open(config_file, "r") as f: + config_data = f.read() + if "instance_group" not in config_data: + with open(config_file, "w") as f: + config_data += "instance_group {\n kind: KIND_CPU\n}\n" + f.write(config_data) + def _remove_config(self, config_file, config_to_remove): # Utility function to remove extra added config from the config files with open(config_file, "r") as f: @@ -125,7 +134,7 @@ def _run_inference_and_validate(self, model): Helper function that takes model as a parameter to verify the corresponding model's stats The passed model is composing model for test case `test_ensemble_composing_model_cache_enabled` For other testcases, the top-level ensemble model stats are verified. - * loads the simple_graphdef_float32_float32_float32 and graphdef_float32_float32_float32 + * loads the simple_onnx_float32_float32_float32 and onnx_float32_float32_float32 and verifies if they are loaded properly. * Checks the initial statistics of the model passed in the parameter Expected - baseline statistics to be all empty metrics since @@ -252,6 +261,8 @@ def test_ensemble_composing_model_cache_enabled(self): self._update_config( self.composing_config_file, RESPONSE_CACHE_PATTERN, RESPONSE_CACHE_CONFIG ) + # Currently, response cache is supported only for tensors on CPU. + self._add_instance_group_cpu(self.composing_config_file) self._run_inference_and_validate(self.composing_model) ensemble_model_stats = self._get_model_statistics(self.ensemble_model) composing_model_stats = self._get_model_statistics(self.composing_model) diff --git a/qa/L0_response_cache/test.sh b/qa/L0_response_cache/test.sh index be78059d1e..c99aac89d1 100755 --- a/qa/L0_response_cache/test.sh +++ b/qa/L0_response_cache/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -64,8 +64,8 @@ ENSEMBLE_CACHE_COMPOSING_DECOUPLED="${MODEL_DIR}/ensemble_cache_composing_decoup rm -fr ${ENSEMBLE_MODEL_DIR} && mkdir ${ENSEMBLE_MODEL_DIR} rm -fr ${ENSEMBLE_CACHE_DECOUPLED} && mkdir ${ENSEMBLE_CACHE_DECOUPLED} rm -fr ${ENSEMBLE_CACHE_COMPOSING_DECOUPLED} && mkdir ${ENSEMBLE_CACHE_COMPOSING_DECOUPLED} -ENSEMBLE_MODEL="simple_graphdef_float32_float32_float32" -COMPOSING_MODEL="graphdef_float32_float32_float32" +ENSEMBLE_MODEL="simple_onnx_float32_float32_float32" +COMPOSING_MODEL="onnx_float32_float32_float32" cp -r "/data/inferenceserver/${REPO_VERSION}/qa_ensemble_model_repository/qa_model_repository/${ENSEMBLE_MODEL}" "${ENSEMBLE_MODEL_DIR}/${ENSEMBLE_MODEL}" cp -r "/data/inferenceserver/${REPO_VERSION}/qa_model_repository/${COMPOSING_MODEL}" "${ENSEMBLE_MODEL_DIR}/${COMPOSING_MODEL}" diff --git a/qa/L0_savedmodel_shape/saved_model_shape_test.py b/qa/L0_savedmodel_shape/saved_model_shape_test.py deleted file mode 100755 index b5ae13a680..0000000000 --- a/qa/L0_savedmodel_shape/saved_model_shape_test.py +++ /dev/null @@ -1,228 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2018-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import sys - -sys.path.append("../common") - -import unittest - -import infer_util as iu -import numpy as np -import test_util as tu - -np_dtype_string = np.dtype(object) - - -class SavedModelShapeTest(tu.TestResultCollector): - def _full_exact( - self, input_dtype, output0_dtype, output1_dtype, output0_raw, output1_raw, swap - ): - def _infer_exact_helper( - tester, - pf, - tensor_shape, - batch_size, - input_dtype, - output0_dtype, - output1_dtype, - output0_raw=True, - output1_raw=True, - model_version=None, - swap=False, - outputs=("OUTPUT0", "OUTPUT1"), - use_http=True, - use_grpc=True, - skip_request_id_check=False, - use_streaming=True, - correlation_id=0, - ): - for bs in (1, batch_size): - # model that does not support batching - if bs == 1: - iu.infer_exact( - tester, - "savedmodel_nobatch", - tensor_shape, - bs, - input_dtype, - output0_dtype, - output1_dtype, - output0_raw=output0_raw, - output1_raw=output1_raw, - model_version=model_version, - swap=swap, - outputs=outputs, - use_http=use_http, - use_grpc=use_grpc, - skip_request_id_check=skip_request_id_check, - use_streaming=use_streaming, - correlation_id=correlation_id, - ) - # model that supports batching - iu.infer_exact( - tester, - "savedmodel", - (bs,) + tensor_shape, - bs, - input_dtype, - output0_dtype, - output1_dtype, - output0_raw=output0_raw, - output1_raw=output1_raw, - model_version=model_version, - swap=swap, - outputs=outputs, - use_http=use_http, - use_grpc=use_grpc, - skip_request_id_check=skip_request_id_check, - use_streaming=use_streaming, - correlation_id=correlation_id, - ) - - input_size = 16 - - if tu.validate_for_tf_model( - input_dtype, - output0_dtype, - output1_dtype, - (input_size,), - (input_size,), - (input_size,), - ): - _infer_exact_helper( - self, - "savedmodel", - (input_size,), - 8, - input_dtype, - output0_dtype, - output1_dtype, - output0_raw=output0_raw, - output1_raw=output1_raw, - swap=swap, - ) - - def test_raw_bbb(self): - self._full_exact( - np.int8, np.int8, np.int8, output0_raw=True, output1_raw=True, swap=True - ) - - def test_raw_sss(self): - self._full_exact( - np.int16, np.int16, np.int16, output0_raw=True, output1_raw=True, swap=True - ) - - def test_raw_iii(self): - self._full_exact( - np.int32, np.int32, np.int32, output0_raw=True, output1_raw=True, swap=True - ) - - def test_raw_lll(self): - self._full_exact( - np.int64, np.int64, np.int64, output0_raw=True, output1_raw=True, swap=False - ) - - def test_raw_hhh(self): - self._full_exact( - np.float16, - np.float16, - np.float16, - output0_raw=True, - output1_raw=True, - swap=False, - ) - - def test_raw_fff(self): - self._full_exact( - np.float32, - np.float32, - np.float32, - output0_raw=True, - output1_raw=True, - swap=True, - ) - - def test_raw_hff(self): - self._full_exact( - np.float16, - np.float32, - np.float32, - output0_raw=True, - output1_raw=True, - swap=False, - ) - - def test_raw_bii(self): - self._full_exact( - np.int8, np.int32, np.int32, output0_raw=True, output1_raw=True, swap=False - ) - - def test_raw_ibb(self): - self._full_exact( - np.int32, np.int8, np.int8, output0_raw=True, output1_raw=True, swap=False - ) - - def test_raw_ibs(self): - self._full_exact( - np.int32, np.int8, np.int16, output0_raw=True, output1_raw=True, swap=False - ) - - def test_raw_iff(self): - self._full_exact( - np.int32, - np.float32, - np.float32, - output0_raw=True, - output1_raw=True, - swap=False, - ) - - def test_raw_fii(self): - self._full_exact( - np.float32, - np.int32, - np.int32, - output0_raw=True, - output1_raw=True, - swap=False, - ) - - def test_raw_ihs(self): - self._full_exact( - np.int32, - np.float16, - np.int16, - output0_raw=True, - output1_raw=True, - swap=False, - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/qa/L0_savedmodel_shape/test.sh b/qa/L0_savedmodel_shape/test.sh deleted file mode 100755 index e059a5bf0b..0000000000 --- a/qa/L0_savedmodel_shape/test.sh +++ /dev/null @@ -1,102 +0,0 @@ -#!/bin/bash -# Copyright (c) 2018-2021, NVIDIA CORPORATION. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -REPO_VERSION=${NVIDIA_TRITON_SERVER_VERSION} -if [ "$#" -ge 1 ]; then - REPO_VERSION=$1 -fi -if [ -z "$REPO_VERSION" ]; then - echo -e "Repository version must be specified" - echo -e "\n***\n*** Test Failed\n***" - exit 1 -fi -if [ ! -z "$TEST_REPO_ARCH" ]; then - REPO_VERSION=${REPO_VERSION}_${TEST_REPO_ARCH} -fi - -export CUDA_VISIBLE_DEVICES=0 - -TEST_RESULT_FILE='test_results.txt' -CLIENT_LOG_BASE="./client_saved_model_shape" -INFER_TEST=saved_model_shape_test.py -EXPECTED_NUM_TESTS="13" - -DATADIR=`pwd`/models - -SERVER=/opt/tritonserver/bin/tritonserver -# Allow more time to exit. Ensemble brings in too many models -SERVER_ARGS="--model-repository=$DATADIR --exit-timeout-secs=120" -SERVER_LOG_BASE="./server_saved_model_shape" -source ../common/util.sh - -rm -f $SERVER_LOG_BASE* $CLIENT_LOG_BASE* - -RET=0 - -SERVER_LOG=$SERVER_LOG_BASE.${TARGET}.log -CLIENT_LOG=$CLIENT_LOG_BASE.${TARGET}.log - -rm -fr models && \ - cp -r /data/inferenceserver/${REPO_VERSION}/qa_noshape_model_repository models - -create_nop_version_dir `pwd`/models - -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 -fi - -set +e -# python unittest seems to swallow ImportError and still return 0 -# exit code. So need to explicitly check CLIENT_LOG to make sure -# we see some running tests -python $INFER_TEST >$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -else - check_test_results $TEST_RESULT_FILE $EXPECTED_NUM_TESTS - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Result Verification Failed\n***" - RET=1 - fi -fi - -set -e - -kill $SERVER_PID -wait $SERVER_PID - -if [ $RET -eq 0 ]; then - echo -e "\n***\n*** Test Passed\n***" -fi - -exit $RET diff --git a/qa/L0_sequence_batcher/sequence_batcher_test.py b/qa/L0_sequence_batcher/sequence_batcher_test.py index 3e6cfc032a..d6d576da5a 100755 --- a/qa/L0_sequence_batcher/sequence_batcher_test.py +++ b/qa/L0_sequence_batcher/sequence_batcher_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright 2018-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -57,7 +57,7 @@ else: _protocols = ("http",) -BACKENDS = os.environ.get("BACKENDS", "graphdef savedmodel onnx plan custom python") +BACKENDS = os.environ.get("BACKENDS", "onnx plan custom python") ENSEMBLES = bool(int(os.environ.get("ENSEMBLES", 1))) NO_BATCHING = int(os.environ["NO_BATCHING"]) == 1 @@ -126,10 +126,6 @@ def get_datatype(self, trial): return (np.float32,) if "custom" in trial: return (np.int32,) - if "savedmodel" in trial: - return (np.float32, np.bool_) - if "graphdef" in trial: - return (np.dtype(object), np.bool_) # Only test the string data type for ONNX and libtorch models in implicit state if IMPLICIT_STATE: @@ -148,7 +144,6 @@ def get_expected_result(self, expected_result, value, trial, flag_str=None): # information. if ( (not NO_BATCHING and ("custom" not in trial)) - or ("graphdef" in trial) or ("plan" in trial) or ("onnx" in trial) ) or ("libtorch" in trial): diff --git a/qa/L0_sequence_batcher/test.sh b/qa/L0_sequence_batcher/test.sh index ac34458b4e..229a935319 100755 --- a/qa/L0_sequence_batcher/test.sh +++ b/qa/L0_sequence_batcher/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2018-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -87,8 +87,6 @@ if [ "$TEST_JETSON" -eq 1 ]; then MODEL_TRIALS="0 v" fi -TF_VERSION=${TF_VERSION:=2} - # On windows the paths invoked by the script (running in WSL) must use # /mnt/c when needed but the paths on the tritonserver command-line # must be C:/ style. @@ -116,14 +114,14 @@ else fi fi -SERVER_ARGS_EXTRA="--backend-directory=${BACKEND_DIR} --backend-config=tensorflow,version=${TF_VERSION} --log-verbose=1" +SERVER_ARGS_EXTRA="--backend-directory=${BACKEND_DIR} --log-verbose=1" source ../common/util.sh RET=0 # If BACKENDS not specified, set to all -BACKENDS=${BACKENDS:="graphdef savedmodel onnx plan libtorch custom python"} +BACKENDS=${BACKENDS:="onnx plan libtorch custom python"} export BACKENDS # If MODEL_TRIALS not specified set to 0 1 2 4 v @@ -198,10 +196,6 @@ function get_datatype () { local dtype="int32 bool" if [[ $1 == "plan" ]]; then dtype="float32" - elif [[ $1 == "savedmodel" ]]; then - dtype="float32 bool" - elif [[ $1 == "graphdef" ]]; then - dtype="object bool int32" fi # Add type string to the onnx model tests only for implicit state. diff --git a/qa/L0_sequence_corrid_batcher/sequence_corrid_batcher_test.py b/qa/L0_sequence_corrid_batcher/sequence_corrid_batcher_test.py index c9883c9133..956b9241d0 100755 --- a/qa/L0_sequence_corrid_batcher/sequence_corrid_batcher_test.py +++ b/qa/L0_sequence_corrid_batcher/sequence_corrid_batcher_test.py @@ -48,9 +48,9 @@ _model_instances = int(os.environ["MODEL_INSTANCES"]) if _no_batching: - _trials = ("savedmodel_nobatch", "graphdef_nobatch", "plan_nobatch", "onnx_nobatch") + _trials = ("plan_nobatch", "onnx_nobatch") else: - _trials = ("savedmodel", "graphdef", "plan", "onnx") + _trials = ("plan", "onnx") _protocols = ("http", "grpc") _max_sequence_idle_ms = 5000 @@ -67,7 +67,6 @@ def get_expected_result(self, expected_result, corrid, value, trial, flag_str=No # information. if ( (("nobatch" not in trial) and ("custom" not in trial)) - or ("graphdef" in trial) or ("plan" in trial) or ("onnx" in trial) ) or ("libtorch" in trial): diff --git a/qa/L0_sequence_corrid_batcher/test.sh b/qa/L0_sequence_corrid_batcher/test.sh index 3948cd7445..e876419b9b 100755 --- a/qa/L0_sequence_corrid_batcher/test.sh +++ b/qa/L0_sequence_corrid_batcher/test.sh @@ -59,8 +59,6 @@ export CUDA_VISIBLE_DEVICES=0 # models4 - four instances with batch-size 1 rm -fr *.log models{0,1,2,4} && mkdir models4 for m in \ - $DATADIR/qa_dyna_sequence_model_repository/graphdef_dyna_sequence_int32 \ - $DATADIR/qa_dyna_sequence_model_repository/savedmodel_dyna_sequence_int32 \ $DATADIR/qa_dyna_sequence_model_repository/plan_dyna_sequence_int32 \ $DATADIR/qa_dyna_sequence_model_repository/onnx_dyna_sequence_int32 \ $DATADIR/qa_dyna_sequence_model_repository/libtorch_dyna_sequence_int32; do diff --git a/qa/L0_sequence_stress/sequence_stress.py b/qa/L0_sequence_stress/sequence_stress.py index bd71e9bcc2..c8b14cf90b 100755 --- a/qa/L0_sequence_stress/sequence_stress.py +++ b/qa/L0_sequence_stress/sequence_stress.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2019-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -87,12 +87,7 @@ def check_sequence_async( (flag_str, value, expected_result, delay_ms) """ - if ( - ("savedmodel" in trial) - or ("graphdef" in trial) - or ("custom" in trial) - or ("plan" in trial) - ): + if ("custom" in trial) or ("plan" in trial): tensor_shape = ( 1, 1, @@ -176,10 +171,8 @@ def check_sequence_async( def get_datatype(trial): # Get the datatype to use based on what models are available (see test.sh) - if ("plan" in trial) or ("savedmodel" in trial): + if "plan" in trial: return np.float32 - if "graphdef" in trial: - return np.dtype(object) return np.int32 diff --git a/qa/L0_server_status/server_status_test.py b/qa/L0_server_status/server_status_test.py index 53a091e046..9a0f26e7b9 100755 --- a/qa/L0_server_status/server_status_test.py +++ b/qa/L0_server_status/server_status_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2018-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -45,7 +45,7 @@ class ServerMetadataTest(tu.TestResultCollector): def test_basic(self): try: for pair in [("localhost:8000", "http"), ("localhost:8001", "grpc")]: - model_name = "graphdef_int32_int8_int8" + model_name = "libtorch_int32_int8_int8" extensions = [ "classification", "sequence", @@ -129,7 +129,7 @@ def test_unknown_model(self): def test_unknown_model_version(self): try: for pair in [("localhost:8000", "http"), ("localhost:8001", "grpc")]: - model_name = "graphdef_int32_int8_int8" + model_name = "onnx_int32_int8_int8" if pair[1] == "http": triton_client = httpclient.InferenceServerClient( url=pair[0], verbose=True @@ -149,18 +149,18 @@ def test_unknown_model_version(self): except InferenceServerException as ex: self.assertTrue( ex.message().startswith( - "Request for unknown model: 'graphdef_int32_int8_int8' version 99 is not found" + "Request for unknown model: 'onnx_int32_int8_int8' version 99 is not found" ) ) def test_model_latest_infer(self): input_size = 16 tensor_shape = (1, input_size) - platform_name = {"graphdef": "tensorflow_graphdef", "onnx": "onnxruntime_onnx"} + platform_name = {"plan": "tensorrt_plan", "onnx": "onnxruntime_onnx"} # There are 3 versions of *_int32_int32_int32 and all # should be available. - for platform in ("graphdef", "onnx"): + for platform in ("plan", "onnx"): model_name = platform + "_int32_int32_int32" # Initially there should be no version stats.. @@ -316,7 +316,7 @@ def test_model_specific_infer(self): # There are 3 versions of *_float32_float32_float32 but only # versions 1 and 3 should be available. - for platform in ("graphdef", "onnx", "plan"): + for platform in ("libtorch", "onnx", "plan"): tensor_shape = (1, input_size) model_name = platform + "_float32_float32_float32" @@ -439,7 +439,7 @@ def test_model_versions_deleted(self): # version 3 was executed once. Version 2 and 3 models were # deleted from the model repository so now only expect version 1 to # be ready and show stats. - for platform in ("graphdef", "onnx"): + for platform in ("libtorch", "onnx"): model_name = platform + "_int32_int32_int32" try: @@ -513,7 +513,7 @@ def test_model_versions_added(self): # Originally There was version 1 of *_float16_float32_float32. # Version 7 was added so now expect just version 7 to be ready # and provide infer stats. - for platform in ("graphdef",): + for platform in ("plan",): model_name = platform + "_float16_float32_float32" try: @@ -615,7 +615,7 @@ def test_infer_stats_no_model_version(self): # version 3 was executed once. Version 2 and 3 models were # deleted from the model repository so now only expect version 1 to # be ready and show infer stats. - for platform in ("graphdef", "onnx"): + for platform in ("libtorch", "onnx"): model_name = platform + "_int32_int32_int32" try: @@ -723,8 +723,8 @@ def test_infer_stats_no_model(self): stats = infer_stats.model_stats self.assertEqual( len(stats), - 221, - "expected 221 infer stats for all ready versions of all model", + 125, + "expected 125 infer stats for all ready versions of all model", ) except InferenceServerException as ex: diff --git a/qa/L0_server_status/test.sh b/qa/L0_server_status/test.sh index d66b4d5a44..7d896f48fb 100755 --- a/qa/L0_server_status/test.sh +++ b/qa/L0_server_status/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2018-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -84,9 +84,9 @@ fi set -e -rm -fr models/graphdef_int32_int32_int32/2 models/graphdef_int32_int32_int32/3 +rm -fr models/libtorch_int32_int32_int32/2 models/libtorch_int32_int32_int32/3 rm -fr models/onnx_int32_int32_int32/2 models/onnx_int32_int32_int32/3 -cp -r models/graphdef_float16_float32_float32/1 models/graphdef_float16_float32_float32/7 +cp -r models/plan_float16_float32_float32/1 models/plan_float16_float32_float32/7 sleep 3 # Dumping the contents of the models that are currently loaded for debugging purposes diff --git a/qa/L0_simple_ensemble/models/simple/config.pbtxt b/qa/L0_simple_ensemble/models/simple/config.pbtxt index 7e7a178fc1..d5627d3db3 100644 --- a/qa/L0_simple_ensemble/models/simple/config.pbtxt +++ b/qa/L0_simple_ensemble/models/simple/config.pbtxt @@ -1,4 +1,4 @@ -# Copyright (c) 2018-2021, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2018-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -25,7 +25,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. name: "simple" -platform: "tensorflow_graphdef" +platform: "onnxruntime_onnx" max_batch_size: 8 version_policy: { all {} } input [ diff --git a/qa/L0_simple_lib/test.sh b/qa/L0_simple_lib/test.sh index 7045f512ef..714f61f752 100755 --- a/qa/L0_simple_lib/test.sh +++ b/qa/L0_simple_lib/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2019-2021, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -55,7 +55,7 @@ for SIMPLE_CLIENT in simple ; do CLIENT_LOG=$SIMPLE_CLIENT SIMPLE_CLIENT=./$SIMPLE_CLIENT - for trial in graphdef savedmodel onnx libtorch plan; do + for trial in onnx libtorch plan; do full=${trial}_float32_float32_float32 rm -rf $MODELSDIR mkdir -p $MODELSDIR/simple/1 && \ @@ -88,10 +88,10 @@ for SIMPLE_CLIENT in simple ; do set -e done - # Use savedmodel for addsub ensemble + # Use onnx for addsub ensemble mkdir -p $MODELSDIR/simple/1 - cp -r $DATADIR/savedmodel_float32_float32_float32/1/* $MODELSDIR/simple/1/. - cp $DATADIR/savedmodel_float32_float32_float32/config.pbtxt $MODELSDIR/simple/. + cp -r $DATADIR/onnx_float32_float32_float32/1/* $MODELSDIR/simple/1/. + cp $DATADIR/onnx_float32_float32_float32/config.pbtxt $MODELSDIR/simple/. (cd $MODELSDIR/simple && \ sed -i "s/^name:.*/name: \"simple\"/" config.pbtxt && \ sed -i "s/label_filename:.*//" config.pbtxt) diff --git a/qa/L0_socket/models/simple/config.pbtxt b/qa/L0_socket/models/simple/config.pbtxt index 838edd5d55..dd82914e26 100644 --- a/qa/L0_socket/models/simple/config.pbtxt +++ b/qa/L0_socket/models/simple/config.pbtxt @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2021, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -25,7 +25,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. name: "simple" -platform: "tensorflow_graphdef" +platform: "onnxruntime_onnx" max_batch_size: 8 input [ { diff --git a/qa/L0_storage_S3/test.sh b/qa/L0_storage_S3/test.sh index 9f2b67cef4..143b78656e 100755 --- a/qa/L0_storage_S3/test.sh +++ b/qa/L0_storage_S3/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2018-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -45,7 +45,7 @@ CLIENT_LOG_BASE="./client" INFER_TEST="../common/infer_test.py" EXPECTED_NUM_TESTS="3" TEST_RESULT_FILE='test_results.txt' -BACKENDS=${BACKENDS:="graphdef savedmodel onnx libtorch plan"} +BACKENDS=${BACKENDS:="onnx libtorch plan"} # S3 credentials are necessary for this test. Pass via ENV variables aws configure set default.region $AWS_DEFAULT_REGION && \ diff --git a/qa/L0_storage_S3_local/test.sh b/qa/L0_storage_S3_local/test.sh index e60b106b31..3add65df60 100755 --- a/qa/L0_storage_S3_local/test.sh +++ b/qa/L0_storage_S3_local/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2020-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -47,7 +47,7 @@ EXPECTED_NUM_TESTS="3" DATADIR="/data/inferenceserver/${REPO_VERSION}/qa_model_repository" # Used to control which backends are run in infer_test.py -BACKENDS=${BACKENDS:="graphdef savedmodel onnx libtorch plan"} +BACKENDS=${BACKENDS:="onnx libtorch plan"} function run_unit_tests() { echo "Running unit tests: ${INFER_TEST}" @@ -228,7 +228,7 @@ awslocal $ENDPOINT_FLAG s3 rm s3://demo-bucket1.0 --recursive --include "*" && \ # Test with Polling, no model configuration file - with strict model config disabled echo "=== Running autocomplete tests ===" -AUTOCOMPLETE_BACKENDS="savedmodel" +AUTOCOMPLETE_BACKENDS="onnx" export BACKENDS=${AUTOCOMPLETE_BACKENDS} set +e @@ -241,9 +241,8 @@ for BACKEND in ${AUTOCOMPLETE_BACKENDS}; do # Config files specify things expected by unit test like label_filename # and max_batch_size for comparing results, so remove some key fields # for autocomplete to fill that won't break the unit test. - sed -i '/platform:/d' models/${model}/config.pbtxt - sed -i '/data_type:/d' models/${model}/config.pbtxt - sed -i '/dims:/d' models/${model}/config.pbtxt + sed -i '/^input {/,/^}/d' models/${model}/config.pbtxt + sed -i '/^output {/,/^}/d' models/${model}/config.pbtxt done done set -e @@ -275,8 +274,8 @@ awslocal $ENDPOINT_FLAG s3 rm s3://demo-bucket1.0 --recursive --include "*" && \ # Test for multiple model repositories using S3 cloud storage echo "=== Running multiple-model-repository tests ===" -BACKENDS1="graphdef libtorch" -BACKENDS2="onnx plan savedmodel" +BACKENDS1="libtorch" +BACKENDS2="onnx plan" export BACKENDS="$BACKENDS1 $BACKENDS2" set +e diff --git a/qa/L0_storage_azure/test.sh b/qa/L0_storage_azure/test.sh index d4b367780a..805c081679 100755 --- a/qa/L0_storage_azure/test.sh +++ b/qa/L0_storage_azure/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2020-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -81,7 +81,7 @@ rm -f $SERVER_LOG_BASE* $CLIENT_LOG_BASE* RET=0 # Used to control which backends are run in infer_test.py -BACKENDS=${BACKENDS:="graphdef savedmodel onnx libtorch plan"} +BACKENDS=${BACKENDS:="onnx libtorch plan"} function run_unit_tests() { BACKENDS=$BACKENDS python $INFER_TEST >$CLIENT_LOG 2>&1 @@ -252,16 +252,15 @@ sleep 10 # Setup model repository with minimal configs to be autocompleted rm -rf models && mkdir -p models -AUTOCOMPLETE_BACKENDS="savedmodel" +AUTOCOMPLETE_BACKENDS="onnx" for FW in ${AUTOCOMPLETE_BACKENDS}; do for model in ${FW}_float32_float32_float32 ${FW}_object_object_object; do cp -r /data/inferenceserver/${REPO_VERSION}/qa_model_repository/${model} models/ # Config files specify things expected by unit test like label_filename # and max_batch_size for comparing results, so remove some key fields # for autocomplete to fill that won't break the unit test. - sed -i '/platform:/d' models/${model}/config.pbtxt - sed -i '/data_type:/d' models/${model}/config.pbtxt - sed -i '/dims:/d' models/${model}/config.pbtxt + sed -i '/^input {/,/^}/d' models/${model}/config.pbtxt + sed -i '/^output {/,/^}/d' models/${model}/config.pbtxt done done diff --git a/qa/L0_storage_swiftstack/infer_test.py b/qa/L0_storage_swiftstack/infer_test.py index f8a65a01a4..922caeb900 100755 --- a/qa/L0_storage_swiftstack/infer_test.py +++ b/qa/L0_storage_swiftstack/infer_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2021-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -83,28 +83,6 @@ def _infer_exact_helper( input_size = 16 - if tu.validate_for_tf_model( - input_dtype, - output0_dtype, - output1_dtype, - (input_size,), - (input_size,), - (input_size,), - ): - for pf in ["graphdef", "savedmodel"]: - _infer_exact_helper( - self, - pf, - (input_size,), - 8, - input_dtype, - output0_dtype, - output1_dtype, - output0_raw=output0_raw, - output1_raw=output1_raw, - swap=swap, - ) - if tu.validate_for_trt_model( input_dtype, output0_dtype, diff --git a/qa/L0_storage_swiftstack/test.sh b/qa/L0_storage_swiftstack/test.sh index 99fb5610d6..e8c0edf477 100755 --- a/qa/L0_storage_swiftstack/test.sh +++ b/qa/L0_storage_swiftstack/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2021-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -104,11 +104,11 @@ aws s3 rm $BUCKET_URL/ --recursive --include "*" # Now start model tests -for FW in graphdef savedmodel onnx libtorch plan; do +for FW in onnx libtorch plan; do cp -r /data/inferenceserver/${REPO_VERSION}/qa_model_repository/${FW}_float32_float32_float32/ models/ done -for FW in graphdef savedmodel onnx libtorch plan; do +for FW in onnx libtorch plan; do for MC in `ls models/${FW}*/config.pbtxt`; do echo "instance_group [ { kind: KIND_GPU }]" >> $MC done diff --git a/qa/L0_string_io/string_client_test.py b/qa/L0_string_io/string_client_test.py index 16112ac70c..1f44c9913a 100755 --- a/qa/L0_string_io/string_client_test.py +++ b/qa/L0_string_io/string_client_test.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright 2019-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -199,8 +199,7 @@ def _test_bytes(self, model_name): with self.assertRaises(tritonutils.InferenceServerException): self._test_unicode_bytes_dtype(client, model_name, dtype) - def test_tf_unicode_bytes(self): - self._test_bytes("graphdef_nobatch_zero_1_object") + def test_unicode_bytes(self): self._test_bytes("string_identity") diff --git a/qa/L0_string_io/test.sh b/qa/L0_string_io/test.sh index eb45d43ba2..538b1f7e90 100755 --- a/qa/L0_string_io/test.sh +++ b/qa/L0_string_io/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2019-2020, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -52,8 +52,8 @@ source ../common/util.sh rm -f $CLIENT_LOG $SERVER_LOG rm -fr models && mkdir models -cp -r /data/inferenceserver/${REPO_VERSION}/qa_identity_model_repository/graphdef_nobatch_zero_1_object models/. -cp -r ../python_models/string_identity models/. +cp -rv /data/inferenceserver/${REPO_VERSION}/qa_identity_model_repository/onnx_zero_1_object/ models/. +cp -rv ../python_models/string_identity models/. mkdir models/string_identity/1/ mv models/string_identity/model.py models/string_identity/1/model.py diff --git a/qa/L0_tf_gpu_io/test.sh b/qa/L0_tf_gpu_io/test.sh deleted file mode 100755 index 98a5dff1ef..0000000000 --- a/qa/L0_tf_gpu_io/test.sh +++ /dev/null @@ -1,147 +0,0 @@ -#!/bin/bash -# Copyright (c) 2019-2023, NVIDIA CORPORATION. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -REPO_VERSION=${NVIDIA_TRITON_SERVER_VERSION} -if [ "$#" -ge 1 ]; then - REPO_VERSION=$1 -fi -if [ -z "$REPO_VERSION" ]; then - echo -e "Repository version must be specified" - echo -e "\n***\n*** Test Failed\n***" - exit 1 -fi -if [ ! -z "$TEST_REPO_ARCH" ]; then - REPO_VERSION=${REPO_VERSION}_${TEST_REPO_ARCH} -fi - -export CUDA_VISIBLE_DEVICES=0 - -TF_TEST=tf_gpu_io_test.py -BACKENDS=${BACKENDS:="graphdef savedmodel"} - -DATADIR=/data/inferenceserver/${REPO_VERSION} - -SERVER=/opt/tritonserver/bin/tritonserver -source ../common/util.sh - -RET=0 -rm -f ./*.log - -# Test with qa identity TF models -for BACKEND in $BACKENDS; do - MODEL_NAME=${BACKEND}_zero_1_float32 - rm -fr models && mkdir -p models - cp -r $DATADIR/qa_identity_model_repository/${MODEL_NAME} \ - models/${MODEL_NAME}_def && \ - (cd models/${MODEL_NAME}_def && \ - sed -i 's/_zero_1_float32/&_def/' config.pbtxt) && \ - # Enable GPU I/O for TensorFlow model - cp -r models/${MODEL_NAME}_def models/${MODEL_NAME}_gpu && \ - (cd models/${MODEL_NAME}_gpu && \ - sed -i 's/_zero_1_float32_def/_zero_1_float32_gpu/' \ - config.pbtxt && \ - echo "optimization { execution_accelerators { gpu_execution_accelerator : [ { name : \"gpu_io\"} ] } }" >> config.pbtxt) - - SERVER_ARGS="--model-repository=`pwd`/models --log-verbose=1" - SERVER_LOG="${MODEL_NAME}.server.log" - run_server - if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 - fi - - set +e - - python $TF_TEST TfGpuIoTest.test_${MODEL_NAME}_def >> ${BACKEND}.sanity.log 2>&1 - if (( $? != 0 )); then - cat ${BACKEND}.sanity.log - RET=1 - fi - - grep "is GPU tensor: true" $SERVER_LOG >> grep.out.log - if [ $? -eq 0 ]; then - echo -e "\n***\n*** Failed. Expected neither input or output is GPU tensor\n***" - RET=1 - fi - - python $TF_TEST TfGpuIoTest.test_${MODEL_NAME}_gpu >> ${BACKEND}.gpu.sanity.log 2>&1 - if (( $? != 0 )); then - cat ${BACKEND}.gpu.sanity.log - RET=1 - fi - - grep "is GPU tensor: true" $SERVER_LOG >> grep.out.log - if [ $? -ne 0 ]; then - echo -e "\n***\n*** Failed. Expected input and output are GPU tensors\n***" - RET=1 - fi - - set -e - - kill $SERVER_PID - wait $SERVER_PID -done - -# Test savedmodel with mismatched key and name -rm -rf models && mkdir -p models -cp -r $DATADIR/qa_tf_tag_sigdef_repository/sig_tag0 models -(cd models/sig_tag0 && \ - echo "optimization { execution_accelerators { gpu_execution_accelerator : [ { name : \"gpu_io\"} ] } }" >> config.pbtxt) - -SERVER_ARGS="--model-repository=`pwd`/models --log-verbose=1" -SERVER_LOG="sig_tag0.server.log" -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 -fi - -set +e -CLIENT_LOG="sig_tag0.gpu.log" -python $TF_TEST TfGpuIoTest.test_sig_tag0 >> $CLIENT_LOG 2>&1 -if (( $? != 0 )); then - cat $CLIENT_LOG - RET=1 -fi -grep "is GPU tensor: true" $SERVER_LOG >> grep.out.log -if [ $? -ne 0 ]; then - echo -e "\n***\n*** Failed. Expected input and output are GPU tensors\n***" - RET=1 -fi -set -e - -kill $SERVER_PID -wait $SERVER_PID - -if [ $RET -eq 0 ]; then - echo -e "\n***\n*** Test Passed\n***" -else - echo -e "\n***\n*** Test FAILED\n***" -fi -exit $RET diff --git a/qa/L0_tf_gpu_io/tf_gpu_io_test.py b/qa/L0_tf_gpu_io/tf_gpu_io_test.py deleted file mode 100755 index fd3550e434..0000000000 --- a/qa/L0_tf_gpu_io/tf_gpu_io_test.py +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import sys - -sys.path.append("../common") - -import unittest - -import infer_util as iu -import numpy as np -import test_util as tu - -TENSOR_SIZE = 16384 - - -class TfGpuIoTest(tu.TestResultCollector): - def _test_helper( - self, - model_name, - shape, - override_input_names=[], - override_output_names=[], - batching_enabled=False, - ): - try: - bs = 1 - if batching_enabled: - shape = [ - [ - bs, - ] - + shape - ] - iu.infer_zero( - self, - "graphdef", - bs, - np.float32, - shape, - shape, - override_model_name=model_name, - override_input_names=override_input_names, - override_output_names=override_output_names, - ) - - except Exception as ex: - self.assertTrue(False, "unexpected error {}".format(ex)) - - def test_sig_tag0(self): - self._test_helper( - "sig_tag0", - [16], - override_input_names=["INPUT"], - override_output_names=["OUTPUT"], - ) - - def test_graphdef_zero_1_float32_def(self): - self._test_helper( - "graphdef_zero_1_float32_def", [TENSOR_SIZE], batching_enabled=True - ) - - def test_graphdef_zero_1_float32_gpu(self): - self._test_helper( - "graphdef_zero_1_float32_gpu", [TENSOR_SIZE], batching_enabled=True - ) - - def test_savedmodel_zero_1_float32_def(self): - self._test_helper( - "savedmodel_zero_1_float32_def", [TENSOR_SIZE], batching_enabled=True - ) - - def test_savedmodel_zero_1_float32_gpu(self): - self._test_helper( - "savedmodel_zero_1_float32_gpu", [TENSOR_SIZE], batching_enabled=True - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/qa/L0_tf_parameters/test.sh b/qa/L0_tf_parameters/test.sh deleted file mode 100755 index 133b6ef68d..0000000000 --- a/qa/L0_tf_parameters/test.sh +++ /dev/null @@ -1,150 +0,0 @@ -#!/bin/bash -# Copyright 2022-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -REPO_VERSION=${NVIDIA_TRITON_SERVER_VERSION} -if [ "$#" -ge 1 ]; then - REPO_VERSION=$1 -fi -if [ -z "$REPO_VERSION" ]; then - echo -e "Repository version must be specified" - echo -e "\n***\n*** Test Failed\n***" - exit 1 -fi -if [ ! -z "$TEST_REPO_ARCH" ]; then - REPO_VERSION=${REPO_VERSION}_${TEST_REPO_ARCH} -fi -source ../common/util.sh - -export CUDA_VISIBLE_DEVICES=0 - -DATADIR=/data/inferenceserver/${REPO_VERSION}/qa_tf_parameters_repository -TEST_RESULT_FILE='test_results.txt' -CLIENT_LOG="./client.log" -TEST=tf_parameter_test.py -EXPECTED_NUM_TESTS="1" -MODEL_REPOSITORY=`pwd`/models -SERVER=/opt/tritonserver/bin/tritonserver -SERVER_LOG="./inference_server.log" - -RET=0 - -rm -rf $SERVER_LOG $CLIENT_LOG models/ -cp -r $DATADIR models -SERVER_ARGS="--model-repository=$MODEL_REPOSITORY" -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 -fi - -set +e -python $TEST TFParameterTest.test_tf_variable_error>$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -else - check_test_results $TEST_RESULT_FILE $EXPECTED_NUM_TESTS - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Result Verification Failed\n***" - RET=1 - fi -fi -set -e - -kill $SERVER_PID -wait $SERVER_PID - -# Add the initialization operation -echo "{\"init_ops\": [\"init\"]}" > models/graphdef_variable/init_ops.json -echo "parameters: { key: \"TF_INIT_OPS_FILE\" value: { string_value:\"init_ops.json\" }}" >> models/graphdef_variable/config.pbtxt - -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 -fi - -set +e -python $TEST TFParameterTest.test_tf_variable>$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -else - check_test_results $TEST_RESULT_FILE $EXPECTED_NUM_TESTS - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Result Verification Failed\n***" - RET=1 - fi -fi -set -e - -kill $SERVER_PID -wait $SERVER_PID - -# Move the initialization op to the model version folder. -mv models/graphdef_variable/init_ops.json models/graphdef_variable/1/ - -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 -fi - -set +e -python $TEST TFParameterTest.test_tf_variable>$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -else - check_test_results $TEST_RESULT_FILE $EXPECTED_NUM_TESTS - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Result Verification Failed\n***" - RET=1 - fi -fi -set -e - -kill $SERVER_PID -wait $SERVER_PID - -if [ $RET -eq 0 ]; then - echo -e "\n***\n*** Test Passed\n***" -else - cat $CLIENT_LOG - echo -e "\n***\n*** Test FAILED\n***" -fi - -exit $RET diff --git a/qa/L0_tf_parameters/tf_parameter_test.py b/qa/L0_tf_parameters/tf_parameter_test.py deleted file mode 100755 index f1a4621d93..0000000000 --- a/qa/L0_tf_parameters/tf_parameter_test.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2022-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import sys - -sys.path.append("../common") - -import unittest - -import numpy as np -import test_util as tu -import tritonclient.http as tritonhttpclient -import tritonclient.utils - - -class TFParameterTest(tu.TestResultCollector): - def setUp(self): - self._client = tritonhttpclient.InferenceServerClient( - "localhost:8000", verbose=True - ) - - def _infer_helper(self): - # The model has a single variable which is added to the input. Since the - # variable is initialized to zero the input and output must match. - model_name = "graphdef_variable" - input = np.array([10], dtype=np.int32) - - inputs = [] - inputs.append(tritonhttpclient.InferInput("INPUT", input.shape, "INT32")) - inputs[-1].set_data_from_numpy(input) - - outputs = [] - outputs.append(tritonhttpclient.InferRequestedOutput("OUTPUT")) - - results = self._client.infer( - model_name=model_name, inputs=inputs, outputs=outputs - ) - output = results.as_numpy("OUTPUT") - np.testing.assert_array_equal(output, input) - - def test_tf_variable(self): - self._infer_helper() - - def test_tf_variable_error(self): - with self.assertRaises(tritonclient.utils.InferenceServerException) as e: - self._infer_helper() - self.assertIn( - "FAILED_PRECONDITION: Could not find variable VARIABLE. This " - + "could mean that the variable has been deleted. In TF1, it can " - + "also mean the variable is uninitialized.", - e.exception.message(), - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/qa/L0_tf_tag_sigdef/test.sh b/qa/L0_tf_tag_sigdef/test.sh deleted file mode 100755 index 32248c74ad..0000000000 --- a/qa/L0_tf_tag_sigdef/test.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/bin/bash -# Copyright 2021-2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -REPO_VERSION=${NVIDIA_TRITON_SERVER_VERSION} -if [ "$#" -ge 1 ]; then - REPO_VERSION=$1 -fi -if [ -z "$REPO_VERSION" ]; then - echo -e "Repository version must be specified" - echo -e "\n***\n*** Test Failed\n***" - exit 1 -fi -if [ ! -z "$TEST_REPO_ARCH" ]; then - REPO_VERSION=${REPO_VERSION}_${TEST_REPO_ARCH} -fi - -export CUDA_VISIBLE_DEVICES=0 - -TEST_RESULT_FILE='test_results.txt' -CLIENT_LOG="./client.log" -TEST=tf_tag_sigdef_test.py - -DATADIR=/data/inferenceserver/${REPO_VERSION}/qa_tf_tag_sigdef_repository -MODELDIR=`pwd`/models - -rm -rf $SERVER_LOG $CLIENT_LOG $MODELDIR -mkdir $MODELDIR -cp -r $DATADIR/* $MODELDIR - -EXPECTED_NUM_TESTS="4" -SERVER=/opt/tritonserver/bin/tritonserver -SERVER_ARGS="--model-repository=$MODELDIR --exit-timeout-secs=120" -SERVER_LOG="./inference_server.log" -source ../common/util.sh - -RET=0 - -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - if [ `grep -c "configuration expects 2 inputs, model provides 1" $SERVER_LOG` != "0" ]; then - echo -e "*** FAILED: sig_tag_different_io config autocompleted with wrong model tag variant, failed to load.\n" - RET=1 - fi - cat $SERVER_LOG - exit 1 -fi - -set +e -python $TEST>$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -else - check_test_results $TEST_RESULT_FILE $EXPECTED_NUM_TESTS - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Result Verification Failed\n***" - RET=1 - fi -fi -set -e - -kill $SERVER_PID -wait $SERVER_PID - -if [ $RET -eq 0 ]; then - echo -e "\n***\n*** Test Passed\n***" -else - cat $CLIENT_LOG - echo -e "\n***\n*** Test FAILED\n***" -fi - -exit $RET diff --git a/qa/L0_tf_tag_sigdef/tf_tag_sigdef_test.py b/qa/L0_tf_tag_sigdef/tf_tag_sigdef_test.py deleted file mode 100755 index b4a11ac04e..0000000000 --- a/qa/L0_tf_tag_sigdef/tf_tag_sigdef_test.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2021-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import sys - -sys.path.append("../common") - -import unittest - -import numpy as np -import test_util as tu -import tritonhttpclient as httpclient - - -class TagSigdefTest(tu.TestResultCollector): - base_model_name = "sig_tag" - base_tag = "serve" - test_tag = "testTag" - base_sig_def = "serving_default" - test_sig_def = "testSigDef" - dims = 16 - - def _test_helper(self, modelVersion, tag, sig_def): - shape = [self.dims] - model_name = self.base_model_name + str(modelVersion) - # The multiplier is defined during model creation. See server/qa/common/gen_tag_sigdef.py - # for details - multiplier = modelVersion + 1 - output_name = "OUTPUT" - triton_client = httpclient.InferenceServerClient("localhost:8000", verbose=True) - inputs = [] - outputs = [] - inputs.append(httpclient.InferInput("INPUT", shape, "FP32")) - input_data = np.ones(shape=shape).astype(np.float32) - inputs[0].set_data_from_numpy(input_data, binary_data=True) - - outputs.append(httpclient.InferRequestedOutput(output_name, binary_data=True)) - results = triton_client.infer(model_name, inputs, outputs=outputs) - output_data = results.as_numpy(output_name) - test_output = input_data * multiplier - self.assertTrue(np.isclose(output_data, test_output).all()) - - def test_default(self): - self._test_helper(0, self.base_tag, self.base_sig_def) - - def test_sig_def(self): - self._test_helper(1, self.base_tag, self.test_sig_def) - - def test_tag(self): - self._test_helper(2, self.test_tag, self.base_sig_def) - - def test_tag_sig_def(self): - self._test_helper(3, self.test_tag, self.test_sig_def) - - -if __name__ == "__main__": - unittest.main() diff --git a/qa/L0_tf_unknown_rank/test.sh b/qa/L0_tf_unknown_rank/test.sh deleted file mode 100755 index e279a46267..0000000000 --- a/qa/L0_tf_unknown_rank/test.sh +++ /dev/null @@ -1,127 +0,0 @@ -#!/bin/bash -# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -REPO_VERSION=${NVIDIA_TRITON_SERVER_VERSION} -if [ "$#" -ge 1 ]; then - REPO_VERSION=$1 -fi -if [ -z "$REPO_VERSION" ]; then - echo -e "Repository version must be specified" - echo -e "\n***\n*** Test Failed\n***" - exit 1 -fi -if [ ! -z "$TEST_REPO_ARCH" ]; then - REPO_VERSION=${REPO_VERSION}_${TEST_REPO_ARCH} -fi - -TEST_RESULT_FILE='test_results.txt' -export CUDA_VISIBLE_DEVICES=0 - -DATADIR=/data/inferenceserver/${REPO_VERSION} - -CLIENT_LOG="./client.log" -UNKNOWN_RANK_TEST=tf_unknown_rank_test.py - -SERVER=/opt/tritonserver/bin/tritonserver -SERVER_ARGS="--model-repository=`pwd`/models" -SERVER_LOG="./inference_server.log" -source ../common/util.sh - -rm -f ./*.log -rm -fr models && mkdir -p models -cp -r $DATADIR/tf_model_store2/unknown_rank_* models/ - -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 -fi - -RET=0 - -set +e -python $UNKNOWN_RANK_TEST UnknownRankTest.test_success >> $CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - echo -e "\n***\n*** Test Failed\n***" - cat $CLIENT_LOG - RET=1 -else - check_test_results $TEST_RESULT_FILE 1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Result Verification Failed\n***" - RET=1 - fi -fi - -python $UNKNOWN_RANK_TEST UnknownRankTest.test_wrong_input >> $CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - echo -e "\n***\n*** Test Failed\n***" - cat $CLIENT_LOG - RET=1 -else - check_test_results $TEST_RESULT_FILE 1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Result Verification Failed\n***" - RET=1 - fi -fi - -set -e - -kill $SERVER_PID -wait $SERVER_PID - -# Try to load model with scalar tensor. The server should fail to load the model. -rm -rf scalar_repo; mkdir scalar_repo -cp -r $DATADIR/tf_model_store3/scalar_model scalar_repo/ -SERVER_ARGS="--model-repository=`pwd`/scalar_repo --strict-model-config=false" -run_server -if [ "$SERVER_PID" != "0" ]; then - echo -e "*** FAILED: unexpected success starting $SERVER" >> $CLIENT_LOG - RET=1 - kill $SERVER_PID - wait $SERVER_PID -else - ERROR_MESSAGE="Unable to autofill for 'scalar_model': the rank of model tensor 'x' is 0 and dimensions are not defined" - if [[ $(cat $SERVER_LOG | grep "${ERROR_MESSAGE}" | wc -l) -ne 2 ]]; then - echo -e "\n***\n*** Test Failed: "${ERROR_MESSAGE}" not found\n***" - cat $SERVER_LOG - RET=1 - fi -fi - - -if [ $RET -eq 0 ]; then - echo -e "\n***\n*** Test Passed\n***" -else - echo -e "\n***\n*** Test FAILED\n***" -fi - -exit $RET diff --git a/qa/L0_tf_unknown_rank/tf_unknown_rank_test.py b/qa/L0_tf_unknown_rank/tf_unknown_rank_test.py deleted file mode 100755 index add6b32c13..0000000000 --- a/qa/L0_tf_unknown_rank/tf_unknown_rank_test.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2022-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import sys - -sys.path.append("../common") - -import unittest - -import numpy as np -import test_util as tu -import tritonhttpclient -from tritonclientutils import * - - -class UnknownRankTest(tu.TestResultCollector): - # helper function to generate requests to the server - def infer_unknown(self, model_name, tensor_shape): - print("About to run the test") - input_data = np.random.random_sample(tensor_shape).astype(np.float32) - client = tritonhttpclient.InferenceServerClient("localhost:8000") - inputs = [ - tritonhttpclient.InferInput( - "INPUT", input_data.shape, np_to_triton_dtype(input_data.dtype) - ) - ] - inputs[0].set_data_from_numpy(input_data) - results = client.infer(model_name, inputs) - self.assertTrue(np.array_equal(results.as_numpy("OUTPUT"), input_data)) - - def test_success(self): - model_name = "unknown_rank_success" - tensor_shape = 1 - try: - self.infer_unknown(model_name, tensor_shape) - except InferenceServerException as ex: - self.assertTrue(False, "unexpected error {}".format(ex)) - - def test_wrong_input(self): - model_name = "unknown_rank_wrong_output" - tensor_shape = (1, 2) - try: - self.infer_unknown(model_name, tensor_shape) - self.fail( - "Found success when expected failure with model given " - "wrong input tensor [1,2] for input [-1,1]." - ) - except InferenceServerException as ex: - self.assertIn( - "unexpected shape for input 'INPUT' for model " - "'unknown_rank_wrong_output'. Expected [1], got [1,2]", - ex.message(), - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/qa/L0_tftrt_optimization/test.sh b/qa/L0_tftrt_optimization/test.sh deleted file mode 100755 index 04dcdc2f65..0000000000 --- a/qa/L0_tftrt_optimization/test.sh +++ /dev/null @@ -1,212 +0,0 @@ -#!/bin/bash -# Copyright (c) 2019-2020, NVIDIA CORPORATION. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -REPO_VERSION=${NVIDIA_TRITON_SERVER_VERSION} -if [ "$#" -ge 1 ]; then - REPO_VERSION=$1 -fi -if [ -z "$REPO_VERSION" ]; then - echo -e "Repository version must be specified" - echo -e "\n***\n*** Test Failed\n***" - exit 1 -fi -if [ ! -z "$TEST_REPO_ARCH" ]; then - REPO_VERSION=${REPO_VERSION}_${TEST_REPO_ARCH} -fi - -export CUDA_VISIBLE_DEVICES=0 - -TEST_RESULT_FILE='test_results.txt' -DATADIR=/data/inferenceserver/${REPO_VERSION} - -CLIENT_LOG="./client.log" -TFTRT_OPTIMIZATION_TEST=tftrt_optimization_test.py - -SERVER=/opt/tritonserver/bin/tritonserver -SERVER_ARGS="--model-repository=`pwd`/models --log-verbose=1 --exit-on-error=false" -SERVER_LOG="./inference_server.log" -source ../common/util.sh - -RET=0 - -for MODEL in \ - graphdef_float32_float32_float32 \ - savedmodel_float32_float32_float32; do - rm -f ./*.log - rm -fr models && mkdir -p models - cp -r $DATADIR/qa_model_repository/${MODEL} \ - models/${MODEL}_def && \ - rm -fr models/${MODEL}_def/2 && \ - rm -fr models/${MODEL}_def/3 && \ - (cd models/${MODEL}_def && \ - sed -i 's/_float32_float32_float32/&_def/' config.pbtxt) && \ - # GPU execution accelerators with default setting - cp -r models/${MODEL}_def models/${MODEL}_trt && \ - (cd models/${MODEL}_trt && \ - sed -i 's/_float32_def/_float32_trt/' \ - config.pbtxt && \ - echo "optimization { execution_accelerators { gpu_execution_accelerator : [ { name : \"tensorrt\"} ] } }" >> config.pbtxt) && \ - # GPU execution accelerators with correct parameters - cp -r models/${MODEL}_def models/${MODEL}_param && \ - (cd models/${MODEL}_param && \ - sed -i 's/_float32_def/_float32_param/' \ - config.pbtxt && \ - echo "optimization { execution_accelerators { gpu_execution_accelerator : [ { name : \"tensorrt\" \ - parameters { key: \"precision_mode\" value: \"FP16\" } \ - parameters { key: \"minimum_segment_size\" value: \"1\" } }]}}" \ - >> config.pbtxt) && \ - # GPU execution accelerators with unknown parameters - cp -r models/${MODEL}_def models/${MODEL}_unknown_param && \ - (cd models/${MODEL}_unknown_param && \ - sed -i 's/_float32_def/_float32_unknown_param/' \ - config.pbtxt && \ - echo "optimization { execution_accelerators { gpu_execution_accelerator : [ { name : \"tensorrt\" \ - parameters { key: \"precision_mode\" value: \"FP16\" } \ - parameters { key: \"segment_size\" value: \"1\" } }]}}" \ - >> config.pbtxt) && \ - # GPU execution accelerators with invalid parameters - cp -r models/${MODEL}_def models/${MODEL}_invalid_param && \ - (cd models/${MODEL}_invalid_param && \ - sed -i 's/_float32_def/_float32_invalid_param/' \ - config.pbtxt && \ - echo "optimization { execution_accelerators { gpu_execution_accelerator : [ { name : \"tensorrt\" \ - parameters { key: \"precision_mode\" value: \"FP16\" } \ - parameters { key: \"max_workspace_size_bytes\" value: \"abc\" } }]}}" \ - >> config.pbtxt) && \ - # GPU execution accelerators on CPU context - cp -r models/${MODEL}_trt models/${MODEL}_cpu_trt && \ - (cd models/${MODEL}_cpu_trt && \ - sed -i 's/_float32_trt/_float32_cpu_trt/' \ - config.pbtxt && \ - echo "instance_group [ { kind: KIND_CPU }]" >> config.pbtxt) && \ - # CPU execution accelerators - cp -r models/${MODEL}_def models/${MODEL}_openvino && \ - (cd models/${MODEL}_openvino && \ - sed -i 's/_float32_def/_float32_openvino/' \ - config.pbtxt && \ - echo "optimization { execution_accelerators { cpu_execution_accelerator : [ { name : \"openvino\" } ] } }" >> config.pbtxt) && \ - # Unknown GPU execution accelerator - cp -r models/${MODEL}_def models/${MODEL}_unknown_gpu && \ - (cd models/${MODEL}_unknown_gpu && \ - sed -i 's/_float32_def/_float32_unknown_gpu/' \ - config.pbtxt && \ - echo "optimization { execution_accelerators { gpu_execution_accelerator : [ { name : \"unknown_gpu\" } ] } }" >> config.pbtxt) && \ - # Unknown CPU execution accelerators - cp -r models/${MODEL}_def models/${MODEL}_unknown_cpu && \ - (cd models/${MODEL}_unknown_cpu && \ - sed -i 's/_float32_def/_float32_unknown_cpu/' \ - config.pbtxt && \ - echo "optimization { execution_accelerators { cpu_execution_accelerator : [ { name : \"unknown_cpu\" } ] } }" >> config.pbtxt) - - run_server_tolive - if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 - fi - - set +e - - grep "TensorRT Execution Accelerator is set for ${MODEL}_trt" $SERVER_LOG - if [ $? -ne 0 ]; then - echo -e "\n***\n*** Failed. Expected TensorRT Execution Accelerator is set\n***" - RET=1 - fi - - grep "TensorRT Execution Accelerator is set for ${MODEL}_param" $SERVER_LOG - if [ $? -ne 0 ]; then - echo -e "\n***\n*** Failed. Expected TensorRT Execution Accelerator is set\n***" - RET=1 - fi - - grep "failed to load '${MODEL}_unknown_param' version 1: Invalid argument: unknown parameter 'segment_size' is provided for TensorRT Execution Accelerator" $SERVER_LOG - if [ $? -ne 0 ]; then - echo -e "\n***\n*** Failed. Expected unknown parameter 'segment_size' returns error\n***" - RET=1 - fi - - grep "failed to load '${MODEL}_invalid_param' version 1: Invalid argument: failed to convert 'abc' to long long integral number" $SERVER_LOG - if [ $? -ne 0 ]; then - echo -e "\n***\n*** Failed. Expected invalid parameter 'abc' returns error\n***" - RET=1 - fi - - grep "GPU Execution Accelerator will be ignored for model instance on CPU" $SERVER_LOG - if [ $? -ne 0 ]; then - echo -e "\n***\n*** Failed. Expected logged warning: GPU Execution Accelerator will be ignored for model instance on CPU\n***" - RET=1 - fi - - grep "failed to load '${MODEL}_openvino' version 1: Invalid argument: CPU Execution Accelerator is not supported in TensorFlow backend" $SERVER_LOG - if [ $? -ne 0 ]; then - echo -e "\n***\n*** Failed. Expected CPU Execution Accelerator returns error\n***" - RET=1 - fi - - grep "failed to load '${MODEL}_unknown_gpu' version 1: Invalid argument: unknown Execution Accelerator 'unknown_gpu' is requested" $SERVER_LOG - if [ $? -ne 0 ]; then - echo -e "\n***\n*** Failed. Expected 'unknown_gpu' Execution Accelerator returns error\n***" - RET=1 - fi - grep "failed to load '${MODEL}_unknown_cpu' version 1: Invalid argument: CPU Execution Accelerator is not supported in TensorFlow backend" $SERVER_LOG - if [ $? -ne 0 ]; then - echo -e "\n***\n*** Failed. Expected 'unknown_cpu' Execution Accelerator returns error\n***" - RET=1 - fi - - TEST_TYPE=test_graphdef && \ - [[ "$MODEL" == "savedmodel_float32_float32_float32" ]] && \ - TEST_TYPE=test_savedmodel - echo "Test: $MODEL" >>$CLIENT_LOG - python $TFTRT_OPTIMIZATION_TEST TFTRTOptimizationTest.$TEST_TYPE \ - >>$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - echo -e "\n***\n*** Test Failed\n***" - cat $CLIENT_LOG - RET=1 - else - check_test_results $TEST_RESULT_FILE 1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Result Verification Failed\n***" - RET=1 - fi - fi - - set -e - - kill $SERVER_PID - wait $SERVER_PID -done - -if [ $RET -eq 0 ]; then - echo -e "\n***\n*** Test Passed\n***" -else - echo -e "\n***\n*** Test FAILED\n***" -fi - -exit $RET diff --git a/qa/L0_tftrt_optimization/tftrt_optimization_test.py b/qa/L0_tftrt_optimization/tftrt_optimization_test.py deleted file mode 100755 index 9e59677317..0000000000 --- a/qa/L0_tftrt_optimization/tftrt_optimization_test.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2019-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import sys - -sys.path.append("../common") - -import unittest - -import numpy as np -import test_util as tu -import tritonhttpclient as httpclient - - -class TFTRTOptimizationTest(tu.TestResultCollector): - def setUp(self): - self.input0_ = np.arange(start=0, stop=16, dtype=np.float32).reshape(1, 16) - self.input1_ = np.ones(shape=16, dtype=np.float32).reshape(1, 16) - self.expected_output0_ = self.input0_ + self.input1_ - self.expected_output1_ = self.input0_ - self.input1_ - - def _addsub_infer(self, model_name): - triton_client = httpclient.InferenceServerClient("localhost:8000", verbose=True) - - inputs = [] - outputs = [] - inputs.append(httpclient.InferInput("INPUT0", [1, 16], "FP32")) - inputs.append(httpclient.InferInput("INPUT1", [1, 16], "FP32")) - - # Initialize the data - inputs[0].set_data_from_numpy(self.input0_, binary_data=True) - inputs[1].set_data_from_numpy(self.input1_, binary_data=False) - - outputs.append(httpclient.InferRequestedOutput("OUTPUT0", binary_data=True)) - outputs.append(httpclient.InferRequestedOutput("OUTPUT1", binary_data=True)) - - results = triton_client.infer(model_name, inputs, outputs=outputs) - - output0_data = results.as_numpy("OUTPUT0") - output1_data = results.as_numpy("OUTPUT1") - - self.assertTrue( - np.array_equal(self.expected_output0_, output0_data), "incorrect sum" - ) - self.assertTrue( - np.array_equal(self.expected_output1_, output1_data), "incorrect difference" - ) - - def test_graphdef(self): - self._addsub_infer("graphdef_float32_float32_float32_trt") - self._addsub_infer("graphdef_float32_float32_float32_param") - - def test_savedmodel(self): - self._addsub_infer("savedmodel_float32_float32_float32_trt") - self._addsub_infer("savedmodel_float32_float32_float32_param") - - -if __name__ == "__main__": - unittest.main() diff --git a/qa/L0_warmup/test.sh b/qa/L0_warmup/test.sh index d307b7ce07..c3e6885062 100755 --- a/qa/L0_warmup/test.sh +++ b/qa/L0_warmup/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -51,7 +51,7 @@ IMAGE="../images/vulture.jpeg" DATADIR=`pwd`/models # If BACKENDS not specified, set to all -BACKENDS=${BACKENDS:="graphdef savedmodel onnx libtorch plan"} +BACKENDS=${BACKENDS:="onnx libtorch plan"} SERVER=/opt/tritonserver/bin/tritonserver SERVER_ARGS="--model-repository=$DATADIR --log-verbose=1 --exit-timeout-secs=120" @@ -177,7 +177,7 @@ for BACKEND in ${BACKENDS}; do # Test for variable-size data type (string) rm -fr models && mkdir models - SUPPORT_STRING=0 && ([[ $BACKEND == "savedmodel" ]] || [[ $BACKEND == "onnx" ]] || [[ $BACKEND == "savedmodel" ]]) && SUPPORT_STRING=1 + SUPPORT_STRING=0 && ([[ $BACKEND == "onnx" ]]) && SUPPORT_STRING=1 if [ "$SUPPORT_STRING" == "1" ] ; then cp -r /data/inferenceserver/${REPO_VERSION}/qa_sequence_model_repository/${BACKEND}_sequence_object models/. cp -r /data/inferenceserver/${REPO_VERSION}/qa_identity_model_repository/${BACKEND}_zero_1_object models/. @@ -287,80 +287,85 @@ for BACKEND in ${BACKENDS}; do wait $SERVER_PID fi - if [ "$BACKEND" == "graphdef" ]; then - # Show effect of warmup by using a TF model with TF-TRT optimization which is - # known to be slow on first inference. - # Note: model can be obatined via the fetching script in docs/example - rm -fr models && \ - mkdir models && \ - cp -r /data/inferenceserver/${REPO_VERSION}/tf_model_store/inception_v3_graphdef models/. - - # Enable TF-TRT optimization - (cd models/inception_v3_graphdef && \ - echo "optimization { execution_accelerators { gpu_execution_accelerator : [ { name : \"tensorrt\"} ] } }" >> config.pbtxt) - - # Duplicate the same model with warmup enabled - cp -r models/inception_v3_graphdef models/inception_v3_warmup && - (cd models/inception_v3_warmup && \ - sed -i 's/inception_v3_graphdef/inception_v3_warmup/' config.pbtxt) - - (cd models/inception_v3_warmup && \ - echo 'model_warmup [{' >> config.pbtxt && \ - echo ' name : "image sample"' >> config.pbtxt && \ - echo ' batch_size: 1' >> config.pbtxt && \ - echo ' inputs {' >> config.pbtxt && \ - echo ' key: "input"' >> config.pbtxt && \ - echo ' value: {' >> config.pbtxt && \ - echo ' data_type: TYPE_FP32' >> config.pbtxt && \ - echo ' dims: [ 299, 299, 3 ]' >> config.pbtxt && \ - echo ' input_data_file: "raw_mug_data"' >> config.pbtxt && \ - echo ' }' >> config.pbtxt && \ - echo ' }' >> config.pbtxt && \ - echo '}]' >> config.pbtxt ) - - # prepare provided data instead of synthetic one - mkdir -p models/inception_v3_warmup/warmup && \ - cp raw_mug_data models/inception_v3_warmup/warmup/. - - run_server - if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 - fi - - set +e - - grep "is running warmup sample 'image sample'" $SERVER_LOG - if [ $? -ne 0 ]; then - echo -e "\n***\n*** Failed. Expected warmup for image model\n***" - RET=1 - fi - grep "failed to run warmup" $SERVER_LOG - if [ $? -eq 0 ]; then - echo -e "\n***\n*** Failed. Expected no warmup error\n***" - RET=1 - fi - - # Time the first inference for both models - time $CLIENT -m inception_v3_graphdef -s INCEPTION $IMAGE -i grpc -u localhost:8001 >>$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - echo -e "\n***\n*** Test Failed\n***" - cat $CLIENT_LOG - RET=1 - fi - time $CLIENT -m inception_v3_warmup -s INCEPTION $IMAGE -i grpc -u localhost:8001 >>$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - echo -e "\n***\n*** Test Failed\n***" - cat $CLIENT_LOG - RET=1 - fi - - set -e - - kill $SERVER_PID - wait $SERVER_PID - fi + # FIXME: This section of code doesn't check if the warmup model + # is faster than the fresh model. Thus we are not losing any coverage + # by commenting it out. The functionality of the warmup methods are + # covered by other parts of this test which will fail if the *functionality* + # breaks. + # if [ "$BACKEND" == "graphdef" ]; then + # # Show effect of warmup by using a TF model with TF-TRT optimization which is + # # known to be slow on first inference. + # # Note: model can be obatined via the fetching script in docs/example + # rm -fr models && \ + # mkdir models && \ + # cp -r /data/inferenceserver/${REPO_VERSION}/tf_model_store/inception_v3_graphdef models/. + + # # Enable TF-TRT optimization + # (cd models/inception_v3_graphdef && \ + # echo "optimization { execution_accelerators { gpu_execution_accelerator : [ { name : \"tensorrt\"} ] } }" >> config.pbtxt) + + # # Duplicate the same model with warmup enabled + # cp -r models/inception_v3_graphdef models/inception_v3_warmup && + # (cd models/inception_v3_warmup && \ + # sed -i 's/inception_v3_graphdef/inception_v3_warmup/' config.pbtxt) + + # (cd models/inception_v3_warmup && \ + # echo 'model_warmup [{' >> config.pbtxt && \ + # echo ' name : "image sample"' >> config.pbtxt && \ + # echo ' batch_size: 1' >> config.pbtxt && \ + # echo ' inputs {' >> config.pbtxt && \ + # echo ' key: "input"' >> config.pbtxt && \ + # echo ' value: {' >> config.pbtxt && \ + # echo ' data_type: TYPE_FP32' >> config.pbtxt && \ + # echo ' dims: [ 299, 299, 3 ]' >> config.pbtxt && \ + # echo ' input_data_file: "raw_mug_data"' >> config.pbtxt && \ + # echo ' }' >> config.pbtxt && \ + # echo ' }' >> config.pbtxt && \ + # echo '}]' >> config.pbtxt ) + + # # prepare provided data instead of synthetic one + # mkdir -p models/inception_v3_warmup/warmup && \ + # cp raw_mug_data models/inception_v3_warmup/warmup/. + + # run_server + # if [ "$SERVER_PID" == "0" ]; then + # echo -e "\n***\n*** Failed to start $SERVER\n***" + # cat $SERVER_LOG + # exit 1 + # fi + + # set +e + + # grep "is running warmup sample 'image sample'" $SERVER_LOG + # if [ $? -ne 0 ]; then + # echo -e "\n***\n*** Failed. Expected warmup for image model\n***" + # RET=1 + # fi + # grep "failed to run warmup" $SERVER_LOG + # if [ $? -eq 0 ]; then + # echo -e "\n***\n*** Failed. Expected no warmup error\n***" + # RET=1 + # fi + + # # Time the first inference for both models + # time $CLIENT -m inception_v3_graphdef -s INCEPTION $IMAGE -i grpc -u localhost:8001 >>$CLIENT_LOG 2>&1 + # if [ $? -ne 0 ]; then + # echo -e "\n***\n*** Test Failed\n***" + # cat $CLIENT_LOG + # RET=1 + # fi + # time $CLIENT -m inception_v3_warmup -s INCEPTION $IMAGE -i grpc -u localhost:8001 >>$CLIENT_LOG 2>&1 + # if [ $? -ne 0 ]; then + # echo -e "\n***\n*** Test Failed\n***" + # cat $CLIENT_LOG + # RET=1 + # fi + + # set -e + + # kill $SERVER_PID + # wait $SERVER_PID + # fi done # Test warmup sample failure diff --git a/qa/common/busy_op_kernel.cc b/qa/common/busy_op_kernel.cc deleted file mode 100644 index 119ed0a1ce..0000000000 --- a/qa/common/busy_op_kernel.cc +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions -// are met: -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in the -// documentation and/or other materials provided with the distribution. -// * Neither the name of NVIDIA CORPORATION nor the names of its -// contributors may be used to endorse or promote products derived -// from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -// OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -#include - -#include "tensorflow/core/framework/device_base.h" -#include "tensorflow/core/framework/op.h" -#include "tensorflow/core/framework/op_kernel.h" -#include "tensorflow/core/framework/tensor_shape.h" - -using namespace tensorflow; // NOLINT(build/namespaces) - -REGISTER_OP("BusyLoop").Input("input: int32").Output("output: int32").Doc(R"doc( -Busy waits for input number of clock cycles -)doc"); - -void BusyLoopKernelLauncher( - const Eigen::GpuDevice& device, const int* num_delay_cycles, int* out); - -class BusyLoopOp : public OpKernel { - public: - explicit BusyLoopOp(OpKernelConstruction* context) : OpKernel(context) {} - - void Compute(OpKernelContext* context) override - { - // Grab the input - const Tensor& input_tensor = context->input(0); - auto num_delay_cycles = input_tensor.flat(); - - // Create dummy output - Tensor* output_tensor = nullptr; - OP_REQUIRES_OK( - context, - context->allocate_output(0, input_tensor.shape(), &output_tensor)); - auto output = output_tensor->template flat(); - - // Verify input dimension - OP_REQUIRES( - context, TensorShapeUtils::IsVector(input_tensor.shape()), - errors::InvalidArgument( - "BusyLoop expects a single value as a 1-D Vector")); - - // Call the cuda kernel launcher - BusyLoopKernelLauncher( - context->eigen_device(), num_delay_cycles.data(), - output.data()); - } -}; - -REGISTER_KERNEL_BUILDER(Name("BusyLoop").Device(DEVICE_GPU), BusyLoopOp); diff --git a/qa/common/check_copyright.py b/qa/common/check_copyright.py index 95694dc460..99606a1c81 100755 --- a/qa/common/check_copyright.py +++ b/qa/common/check_copyright.py @@ -63,7 +63,6 @@ "docs/repositories.txt", "docs/exclusions.txt", "docker", - "qa/common/cuda_op_kernel.cu.cc.patch", "qa/ensemble_models/mix_platform_float32_float32_float32/output0_labels.txt", "qa/ensemble_models/mix_type_int32_float32_float32/output0_labels.txt", "qa/ensemble_models/mix_ensemble_int32_float32_float32/output0_labels.txt", diff --git a/qa/common/check_valgrind_log.py b/qa/common/check_valgrind_log.py index 201d0e922c..84eecd4989 100755 --- a/qa/common/check_valgrind_log.py +++ b/qa/common/check_valgrind_log.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2021-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -31,19 +31,12 @@ # Check the valgrind logs for memory leaks, ignoring known memory leaks # * cnmem https://github.com/NVIDIA/cnmem/issues/12 -# * Tensorflow::NewSession # * dl-open leak could be due to https://bugs.kde.org/show_bug.cgi?id=358980 -# * dlerror leak in tensorflow::HadoopFileSystem::HadoopFileSystem() -# -> tensorflow::LibHDFS::LoadAndBind()::{lambda(char const*, void**)#1}::operator()(char const*, void**) -# -> tensorflow::internal::LoadLibrary -# -> dlerror LEAK_WHITE_LIST = [ "cnmem", - "tensorflow::NewSession", "dl-init", "dl-open", - "dlerror", "libtorch", ] diff --git a/qa/common/cuda_op_kernel.cu.cc.patch b/qa/common/cuda_op_kernel.cu.cc.patch deleted file mode 100644 index e28072670d..0000000000 --- a/qa/common/cuda_op_kernel.cu.cc.patch +++ /dev/null @@ -1,33 +0,0 @@ -diff --git a/tensorflow/examples/adding_an_op/cuda_op_kernel.cu.cc b/tensorflow/examples/adding_an_op/cuda_op_kernel.cu.cc -index a9d66f9..a92e218 100644 ---- a/tensorflow/examples/adding_an_op/cuda_op_kernel.cu.cc -+++ b/tensorflow/examples/adding_an_op/cuda_op_kernel.cu.cc -@@ -14,10 +14,12 @@ limitations under the License. - ==============================================================================*/ - - #if GOOGLE_CUDA --#define EIGEN_USE_GPU --#include "unsupported/Eigen/CXX11/Tensor" // from @eigen_archive --#include "tensorflow/core/util/gpu_kernel_helper.h" --#include "tensorflow/core/util/gpu_launch_config.h" -+//#define EIGEN_USE_GPU -+//#include "unsupported/Eigen/CXX11/Tensor" -+//#include "tensorflow/core/util/gpu_kernel_helper.h" -+//#include "tensorflow/core/util/gpu_launch_config.h" -+#include -+#include - - __global__ void AddOneKernel(const int* in, const int N, int* out) { - for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < N; -@@ -27,8 +29,9 @@ __global__ void AddOneKernel(const int* in, const int N, int* out) { - } - - void AddOneKernelLauncher(const int* in, const int N, int* out) { -- TF_CHECK_OK(::tensorflow::GpuLaunchKernel(AddOneKernel, 32, 256, 0, nullptr, -- in, N, out)); -+ int block_size = std::min(N, 1024); -+ int grid_size = (N + block_size - 1) / block_size; -+ AddOneKernel<<>>(in, N, out); - } - - #endif diff --git a/qa/common/gen_common.py b/qa/common/gen_common.py index 5b7709504b..d53702d604 100644 --- a/qa/common/gen_common.py +++ b/qa/common/gen_common.py @@ -115,34 +115,6 @@ def np_to_trt_dtype(np_dtype): return None -def np_to_tf_dtype(np_dtype): - import tensorflow as tf - - if np_dtype == bool: - return tf.bool - elif np_dtype == np.int8: - return tf.int8 - elif np_dtype == np.int16: - return tf.int16 - elif np_dtype == np.int32: - return tf.int32 - elif np_dtype == np.int64: - return tf.int64 - elif np_dtype == np.uint8: - return tf.uint8 - elif np_dtype == np.uint16: - return tf.uint16 - elif np_dtype == np.float16: - return tf.float16 - elif np_dtype == np.float32: - return tf.float32 - elif np_dtype == np.float64: - return tf.float64 - elif np_dtype == np_dtype_string: - return tf.string - return None - - def np_to_torch_dtype(np_dtype): import torch diff --git a/qa/common/gen_ensemble_model_utils.py b/qa/common/gen_ensemble_model_utils.py index dd4f6e326a..fec8f0bf92 100755 --- a/qa/common/gen_ensemble_model_utils.py +++ b/qa/common/gen_ensemble_model_utils.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2019-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -43,8 +43,6 @@ def fixed_to_variable_size(shape): def platform_types_and_validation(): res = [ - ("graphdef", tu.validate_for_tf_model), - ("savedmodel", tu.validate_for_tf_model), ("plan", tu.validate_for_trt_model), ("onnx", tu.validate_for_onnx_model), ("libtorch", tu.validate_for_libtorch_model), diff --git a/qa/common/gen_jetson_trt_models b/qa/common/gen_jetson_trt_models index 47fd8c758c..1dda5b72cf 100755 --- a/qa/common/gen_jetson_trt_models +++ b/qa/common/gen_jetson_trt_models @@ -34,7 +34,7 @@ # Make all generated files accessible outside of container umask 0000 # Set the version of the models -TRITON_VERSION=${TRITON_VERSION:=25.02} +TRITON_VERSION=${TRITON_VERSION:=25.03} # Set the CUDA device to use CUDA_DEVICE=${RUNNER_ID:=0} # Set TensorRT image @@ -108,7 +108,7 @@ cat > $TRT_MODEL_SCRIPT < $TFSCRIPT <4.24.0" - -TF_CFLAGS=\$(python -c 'import tensorflow as tf; print(" ".join(tf.sysconfig.get_compile_flags()))') -TF_LFLAGS=\$(python -c 'import tensorflow as tf; print(" ".join(tf.sysconfig.get_link_flags()))') - -# No CUDA -cd /tmp -cp /opt/tensorflow/tensorflow-source/tensorflow/examples/adding_an_op/zero_out_op_kernel_1.cc . -g++ -std=${STD_FLAG} -O2 -shared -fPIC zero_out_op_kernel_1.cc -o \$DESTDIR/libzeroout.so \${TF_CFLAGS[@]} \${TF_LFLAGS[@]} - -# CUDA. Need to patch so that we can build it outside of bazel/TF -cp /opt/tensorflow/tensorflow-source/tensorflow/examples/adding_an_op/cuda_op_kernel.cc . -cp /opt/tensorflow/tensorflow-source/tensorflow/examples/adding_an_op/cuda_op_kernel.cu.cc . -patch -i $VOLUME_SRCDIR/cuda_op_kernel.cu.cc.patch cuda_op_kernel.cu.cc -nvcc --expt-relaxed-constexpr -std=${STD_FLAG} -O2 -c -arch=all -o cuda_op_kernel.cu.o cuda_op_kernel.cu.cc \${TF_CFLAGS[@]} -D GOOGLE_CUDA=1 -x cu -Xcompiler -fPIC -g++ -std=${STD_FLAG} -shared -o \$DESTDIR/libcudaop.so cuda_op_kernel.cc cuda_op_kernel.cu.o \${TF_CFLAGS[@]} -fPIC -L/usr/local/cuda/lib64 -lcudart \${TF_LFLAGS[@]} - -cp $VOLUME_SRCDIR/busy_op_kernel.cc . -cp $VOLUME_SRCDIR/busy_op_kernel.cu.cc . -nvcc --expt-relaxed-constexpr -std=${STD_FLAG} -O2 -c -arch=all -o busy_op_kernel.cu.o busy_op_kernel.cu.cc \${TF_CFLAGS[@]} -D GOOGLE_CUDA=1 -x cu -Xcompiler -fPIC -g++ -std=${STD_FLAG} -shared -o \$DESTDIR/libbusyop.so busy_op_kernel.cc busy_op_kernel.cu.o \${TF_CFLAGS[@]} -fPIC -L/usr/local/cuda/lib64 -lcudart \${TF_LFLAGS[@]} - -python3 $VOLUME_SRCDIR/gen_qa_custom_ops_models.py --graphdef --savedmodel \ - --models_dir=\$DESTDIR --zero_out_lib_path=\$DESTDIR/libzeroout.so \ - --cuda_op_lib_path=\$DESTDIR/libcudaop.so \ - --busy_op_lib_path=\$DESTDIR/libbusyop.so -chmod -R 777 \$DESTDIR -EOF - -chmod a+x $TFSCRIPT -if [ $? -ne 0 ]; then - echo -e "Failed: chmod" - exit 1 -fi - -docker cp $TFSCRIPT $DOCKER_VOLUME_CONTAINER:$VOLUME_SRCDIR/$TFSCRIPT - -docker pull $TENSORFLOW_IMAGE - -echo -e "\033[34m[ INFO ] - Running: $TFSCRIPT \033[0m " - -docker run \ - --rm \ - --label RUNNER_ID=$RUNNER_ID \ - --label PROJECT_NAME=$PROJECT_NAME \ - $DOCKER_GPU_ARGS \ - -v $DOCKER_VOLUME:/mnt \ - -e DESTDIR=$VOLUME_DESTDIR/tf_custom_ops \ - $TENSORFLOW_IMAGE \ - bash -xe $VOLUME_SRCDIR/$TFSCRIPT - -if [ $? -ne 0 ]; then - echo -e "Failed" - exit 1 -fi - # PyTorch cat > $PYTSCRIPT < 0 - else "" - ), - "fp32" if dtype == np.float32 else "int32", - "fp32" if dtype == np.float32 else "int32", - "fp32" if dtype == np.float32 else "int32", - np_to_model_dtype(dtype), - tu.shape_to_dims_str(shape), - np_to_model_dtype(dtype), - ) - - try: - os.makedirs(config_dir) - except OSError as ex: - pass # ignore existing dir - - with open(config_dir + "/config.pbtxt", "w") as cfile: - cfile.write(config) - - def create_plan_shape_tensor_modelfile( models_dir, model_version, max_batch, dtype, shape, shape_tensor_input_dtype ): @@ -1433,20 +1161,6 @@ def create_shape_tensor_models( def create_models(models_dir, dtype, shape, no_batch=True): model_version = 1 - if FLAGS.graphdef: - create_tf_modelconfig(False, models_dir, model_version, 8, dtype, shape) - create_tf_modelfile(False, models_dir, model_version, 8, dtype, shape) - if no_batch: - create_tf_modelconfig(False, models_dir, model_version, 0, dtype, shape) - create_tf_modelfile(False, models_dir, model_version, 0, dtype, shape) - - if FLAGS.savedmodel: - create_tf_modelconfig(True, models_dir, model_version, 8, dtype, shape) - create_tf_modelfile(True, models_dir, model_version, 8, dtype, shape) - if no_batch: - create_tf_modelconfig(True, models_dir, model_version, 0, dtype, shape) - create_tf_modelfile(True, models_dir, model_version, 0, dtype, shape) - if FLAGS.tensorrt: suffix = [] if dtype == np.int8: @@ -1485,18 +1199,6 @@ def create_models(models_dir, dtype, shape, no_batch=True): parser.add_argument( "--models_dir", type=str, required=True, help="Top-level model directory" ) - parser.add_argument( - "--graphdef", - required=False, - action="store_true", - help="Generate GraphDef models", - ) - parser.add_argument( - "--savedmodel", - required=False, - action="store_true", - help="Generate SavedModel models", - ) parser.add_argument( "--tensorrt", required=False, @@ -1539,11 +1241,6 @@ def create_models(models_dir, dtype, shape, no_batch=True): ) FLAGS, unparsed = parser.parse_known_args() - if FLAGS.graphdef or FLAGS.savedmodel: - import tensorflow as tf - from tensorflow.python.framework import graph_io - - tf.compat.v1.disable_eager_execution() if FLAGS.tensorrt or FLAGS.tensorrt_shape_io: import tensorrt as trt if FLAGS.onnx: diff --git a/qa/common/gen_qa_identity_models.py b/qa/common/gen_qa_identity_models.py index e74a4f62b7..5fa7b7ab01 100755 --- a/qa/common/gen_qa_identity_models.py +++ b/qa/common/gen_qa_identity_models.py @@ -35,7 +35,6 @@ from gen_common import ( np_to_model_dtype, np_to_onnx_dtype, - np_to_tf_dtype, np_to_trt_dtype, openvino_save_model, ) @@ -45,138 +44,6 @@ from typing import List, Tuple -def create_tf_modelfile( - create_savedmodel, models_dir, model_version, io_cnt, max_batch, dtype, shape -): - if not tu.validate_for_tf_model(dtype, dtype, dtype, shape, shape, shape): - return - - tf_dtype = np_to_tf_dtype(dtype) - - # Create the model that copies inputs to corresponding outputs. - tf.compat.v1.reset_default_graph() - for io_num in range(io_cnt): - input_name = "INPUT{}".format(io_num) - output_name = "OUTPUT{}".format(io_num) - if max_batch == 0: - tin = tf.compat.v1.placeholder( - tf_dtype, tu.shape_to_tf_shape(shape), input_name - ) - else: - tin = tf.compat.v1.placeholder( - tf_dtype, - [ - None, - ] - + tu.shape_to_tf_shape(shape), - input_name, - ) - toutput = tf.identity(tin, name=output_name) - - # Use model name based on io_cnt and non-batching variant - if create_savedmodel: - model_name = tu.get_zero_model_name( - "savedmodel_nobatch" if max_batch == 0 else "savedmodel", io_cnt, dtype - ) - else: - model_name = tu.get_zero_model_name( - "graphdef_nobatch" if max_batch == 0 else "graphdef", io_cnt, dtype - ) - - model_version_dir = os.path.join(models_dir, model_name, str(model_version)) - os.makedirs(model_version_dir, exist_ok=True) - - if create_savedmodel: - with tf.compat.v1.Session() as sess: - input_dict = {} - output_dict = {} - for io_num in range(io_cnt): - input_name = "INPUT{}".format(io_num) - output_name = "OUTPUT{}".format(io_num) - input_tensor = tf.compat.v1.get_default_graph().get_tensor_by_name( - input_name + ":0" - ) - output_tensor = tf.compat.v1.get_default_graph().get_tensor_by_name( - output_name + ":0" - ) - input_dict[input_name] = input_tensor - output_dict[output_name] = output_tensor - tf.compat.v1.saved_model.simple_save( - sess, - model_version_dir + "/model.savedmodel", - inputs=input_dict, - outputs=output_dict, - ) - else: - with tf.compat.v1.Session() as sess: - graph_io.write_graph( - sess.graph.as_graph_def(), - model_version_dir, - "model.graphdef", - as_text=False, - ) - - -def create_tf_modelconfig( - create_savedmodel, models_dir, model_version, io_cnt, max_batch, dtype, shape -): - if not tu.validate_for_tf_model(dtype, dtype, dtype, shape, shape, shape): - return - - shape_str = tu.shape_to_dims_str(shape) - - # Use a different model name for the non-batching variant - if create_savedmodel: - model_name = tu.get_zero_model_name( - "savedmodel_nobatch" if max_batch == 0 else "savedmodel", io_cnt, dtype - ) - else: - model_name = tu.get_zero_model_name( - "graphdef_nobatch" if max_batch == 0 else "graphdef", io_cnt, dtype - ) - - config_dir = os.path.join(models_dir, model_name) - config = """ -name: "{}" -platform: "{}" -max_batch_size: {} -""".format( - model_name, - "tensorflow_savedmodel" if create_savedmodel else "tensorflow_graphdef", - max_batch, - ) - - for io_num in range(io_cnt): - config += """ -input [ - {{ - name: "INPUT{}" - data_type: {} - dims: [ {} ] - }} -] -output [ - {{ - name: "OUTPUT{}" - data_type: {} - dims: [ {} ] - }} -] -""".format( - io_num, - np_to_model_dtype(dtype), - shape_str, - io_num, - np_to_model_dtype(dtype), - shape_str, - ) - - os.makedirs(config_dir, exist_ok=True) - - with open(config_dir + "/config.pbtxt", "w") as cfile: - cfile.write(config) - - def create_ensemble_modelfile( create_savedmodel, models_dir, model_version, io_cnt, max_batch, dtype, shape ): @@ -1107,28 +974,6 @@ def create_shape_tensor_models( def create_models(models_dir, dtype, shape, io_cnt=1, no_batch=True): model_version = 1 - if FLAGS.graphdef: - create_tf_modelconfig(False, models_dir, model_version, io_cnt, 8, dtype, shape) - create_tf_modelfile(False, models_dir, model_version, io_cnt, 8, dtype, shape) - if no_batch: - create_tf_modelconfig( - False, models_dir, model_version, io_cnt, 0, dtype, shape - ) - create_tf_modelfile( - False, models_dir, model_version, io_cnt, 0, dtype, shape - ) - - if FLAGS.savedmodel: - create_tf_modelconfig(True, models_dir, model_version, io_cnt, 8, dtype, shape) - create_tf_modelfile(True, models_dir, model_version, io_cnt, 8, dtype, shape) - if no_batch: - create_tf_modelconfig( - True, models_dir, model_version, io_cnt, 0, dtype, shape - ) - create_tf_modelfile( - True, models_dir, model_version, io_cnt, 0, dtype, shape - ) - if FLAGS.onnx: create_onnx_modelconfig( True, models_dir, model_version, io_cnt, 8, dtype, shape @@ -1222,23 +1067,13 @@ def create_models(models_dir, dtype, shape, io_cnt=1, no_batch=True): ) +# FIXME: The function signatures require a `savedmodel` boolean flag +# on all of them even though Tensorflow has been deprecated since 25.03 if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( "--models_dir", type=str, required=True, help="Top-level model directory" ) - parser.add_argument( - "--graphdef", - required=False, - action="store_true", - help="Generate GraphDef models", - ) - parser.add_argument( - "--savedmodel", - required=False, - action="store_true", - help="Generate SavedModel models", - ) parser.add_argument( "--onnx", required=False, @@ -1296,11 +1131,6 @@ def create_models(models_dir, dtype, shape, io_cnt=1, no_batch=True): ) FLAGS, unparsed = parser.parse_known_args() - if FLAGS.graphdef or FLAGS.savedmodel: - import tensorflow as tf - from tensorflow.python.framework import graph_io - - tf.compat.v1.disable_eager_execution() if FLAGS.onnx: import onnx if FLAGS.libtorch: diff --git a/qa/common/gen_qa_implicit_models.py b/qa/common/gen_qa_implicit_models.py index 793998bdea..c3429d6012 100755 --- a/qa/common/gen_qa_implicit_models.py +++ b/qa/common/gen_qa_implicit_models.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2021-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -1276,18 +1276,6 @@ def create_models(models_dir, dtype, shape, initial_state, no_batch=True): parser.add_argument( "--models_dir", type=str, required=True, help="Top-level model directory" ) - parser.add_argument( - "--graphdef", - required=False, - action="store_true", - help="Generate GraphDef models", - ) - parser.add_argument( - "--savedmodel", - required=False, - action="store_true", - help="Generate SavedModel models", - ) parser.add_argument( "--tensorrt", required=False, diff --git a/qa/common/gen_qa_model_repository b/qa/common/gen_qa_model_repository index 38bebb7f43..0b5e0e9e1d 100755 --- a/qa/common/gen_qa_model_repository +++ b/qa/common/gen_qa_model_repository @@ -28,9 +28,9 @@ ############################################################################ ## This script generates the model repository needed by some of the ## tritonserver CI tests. Generating these models requires using -## the TensorFlow and PyTorch containers. +## the PyTorch container. ## -## 1. Update TENSORRT_IMAGE, PYTORCH_IMAGE and TENSORFLOW_IMAGE to +## 1. Update TENSORRT_IMAGE and PYTORCH_IMAGE to ## match what is being used by the tritonserver release being ## tested. ## @@ -48,7 +48,7 @@ ## ############################################################################ -TRITON_VERSION=${TRITON_VERSION:=25.02} +TRITON_VERSION=${TRITON_VERSION:=25.03} # ONNX. Use ONNX_OPSET 0 to use the default for ONNX version ONNX_VERSION=1.16.1 @@ -59,7 +59,6 @@ OPENVINO_VERSION=2024.5.0 UBUNTU_IMAGE=${UBUNTU_IMAGE:=ubuntu:22.04} PYTORCH_IMAGE=${PYTORCH_IMAGE:=nvcr.io/nvidia/pytorch:$TRITON_VERSION-py3} -TENSORFLOW_IMAGE=${TENSORFLOW_IMAGE:=nvcr.io/nvidia/tensorflow:$TRITON_VERSION-tf2-py3} TENSORRT_IMAGE=${TENSORRT_IMAGE:=nvcr.io/nvidia/tensorrt:$TRITON_VERSION-py3} CUDA_DEVICE=${NV_GPU:=0} @@ -109,9 +108,7 @@ VOLUME_SRCDIR=${VOLUME_SRCDIR:=$VOLUME_BUILD_DIR/gen_srcdir} VOLUME_DESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_model_repository VOLUME_VARDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_variable_model_repository VOLUME_IDENTITYDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_identity_model_repository -VOLUME_SIGDEFDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_tf_tag_sigdef_repository VOLUME_IDENTITYBIGDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_identity_big_model_repository -VOLUME_TFPARAMETERSDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_tf_parameters_repository VOLUME_SHAPEDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_shapetensor_model_repository VOLUME_RESHAPEDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_reshape_model_repository VOLUME_SEQDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_sequence_model_repository @@ -172,7 +169,6 @@ docker run \ ONNXSCRIPT=gen.ONNXRuntime.gen_qa_model_repository.cmds OPENVINOSCRIPT=gen.OpenVINO.gen_qa_model_repository.cmds TORCHSCRIPT=gen.PyTorch.gen_qa_model_repository.cmds -TFSCRIPT=gen.TensorFlow.gen_qa_model_repository.cmds TRTSCRIPT=gen.TensorRT.gen_qa_model_repository.cmds # OPENVINO @@ -184,7 +180,7 @@ cat > $OPENVINOSCRIPT < $ONNXSCRIPT < $TORCHSCRIPT < $TFSCRIPT <4.24.0" - -python3 $VOLUME_SRCDIR/gen_qa_models.py --graphdef --savedmodel --models_dir=$VOLUME_DESTDIR -chmod -R 777 $VOLUME_DESTDIR -python3 $VOLUME_SRCDIR/gen_qa_models.py --graphdef --savedmodel --variable --models_dir=$VOLUME_VARDESTDIR -chmod -R 777 $VOLUME_VARDESTDIR -python3 $VOLUME_SRCDIR/gen_qa_identity_models.py --graphdef --savedmodel --models_dir=$VOLUME_IDENTITYDESTDIR -chmod -R 777 $VOLUME_IDENTITYDESTDIR -python3 $VOLUME_SRCDIR/gen_qa_reshape_models.py --graphdef --savedmodel --variable --models_dir=$VOLUME_RESHAPEDESTDIR -chmod -R 777 $VOLUME_RESHAPEDESTDIR -python3 $VOLUME_SRCDIR/gen_qa_sequence_models.py --graphdef --savedmodel --models_dir=$VOLUME_SEQDESTDIR -chmod -R 777 $VOLUME_SEQDESTDIR -python3 $VOLUME_SRCDIR/gen_qa_sequence_models.py --graphdef --savedmodel --variable --models_dir=$VOLUME_VARSEQDESTDIR -chmod -R 777 $VOLUME_VARSEQDESTDIR -python3 $VOLUME_SRCDIR/gen_qa_dyna_sequence_models.py --graphdef --savedmodel --models_dir=$VOLUME_DYNASEQDESTDIR -chmod -R 777 $VOLUME_DYNASEQDESTDIR -python3 $VOLUME_SRCDIR/gen_qa_noshape_models.py --savedmodel --models_dir=$VOLUME_NOSHAPEDESTDIR -chmod -R 777 $VOLUME_NOSHAPEDESTDIR -python3 $VOLUME_SRCDIR/gen_qa_ragged_models.py --savedmodel --models_dir=$VOLUME_RAGGEDDESTDIR -chmod -R 777 $VOLUME_RAGGEDDESTDIR -python3 $VOLUME_SRCDIR/gen_qa_models.py --ensemble --models_dir=$VOLUME_ENSEMBLEDESTDIR/qa_model_repository -python3 $VOLUME_SRCDIR/gen_qa_models.py --ensemble --variable --models_dir=$VOLUME_ENSEMBLEDESTDIR/qa_variable_model_repository -python3 $VOLUME_SRCDIR/gen_qa_reshape_models.py --ensemble --models_dir=$VOLUME_ENSEMBLEDESTDIR/qa_reshape_model_repository -python3 $VOLUME_SRCDIR/gen_qa_identity_models.py --ensemble --models_dir=$VOLUME_ENSEMBLEDESTDIR/qa_identity_model_repository -python3 $VOLUME_SRCDIR/gen_qa_sequence_models.py --ensemble --models_dir=$VOLUME_ENSEMBLEDESTDIR/qa_sequence_model_repository -python3 $VOLUME_SRCDIR/gen_qa_sequence_models.py --ensemble --variable --models_dir=$VOLUME_ENSEMBLEDESTDIR/qa_variable_sequence_model_repository -chmod -R 777 $VOLUME_ENSEMBLEDESTDIR -python3 $VOLUME_SRCDIR/gen_tag_sigdef.py --dir $VOLUME_SIGDEFDESTDIR -chmod -R 777 $VOLUME_SIGDEFDESTDIR -python3 $VOLUME_SRCDIR/gen_qa_tf_parameters.py --models_dir $VOLUME_TFPARAMETERSDESTDIR -chmod -R 777 $VOLUME_TFPARAMETERSDESTDIR -EOF - -chmod a+x $TFSCRIPT -if [ $? -ne 0 ]; then - echo -e "Failed: chmod" - exit 1 -fi - -docker cp $TFSCRIPT $DOCKER_VOLUME_CONTAINER:$VOLUME_SRCDIR - -docker pull $TENSORFLOW_IMAGE - -echo -e "\033[34m[ INFO ] - Running: $TFSCRIPT \033[0m " - -docker run \ - --rm \ - --label RUNNER_ID=$RUNNER_ID \ - --label PROJECT_NAME=$PROJECT_NAME \ - $DOCKER_GPU_ARGS \ - -v $DOCKER_VOLUME:/mnt \ - $TENSORFLOW_IMAGE \ - bash -xe $VOLUME_SRCDIR/$TFSCRIPT - -if [ $? -ne 0 ]; then - echo -e "Failed" - exit 1 -fi - # TensorRT docker pull ${TENSORRT_IMAGE} @@ -459,7 +392,7 @@ cat > $TRTSCRIPT <_0: tag: "serve", signature_def: "serving_default", multiplier 1 - _1: tag: "serve", signature_def: , multiplier 2 - _2: tag: , signature_def: "serving_default", multiplier 3 - _3: tag: , signature_def: , multiplier 4 - - If different_io is true, there will be two variants of the model created. - The variants will have different numbers of inputs and outputs. - Alternate naming convention and config: - 0: tag: "serve", signature_def: "serving_default", two inputs/outputs - 1: tag: , signature_def: , one input/output - """ - model_version_dir = models_dir + "/" + model_name + "/" + str(model_version) - - try: - os.makedirs(model_version_dir) - except OSError as ex: - pass # ignore existing dir - - with tf.Session() as sess: - input_tensor = tf.placeholder(tf.float32, [dims], "TENSOR_INPUT") - - # tag:"serve", signature_def:"serving_default" - multiplier_0 = tf.constant(1.0, name="multiplier_0") - # tag:"serve", signature_def:signature_def_name - multiplier_1 = tf.constant(2.0, name="multiplier_1") - # tag:tag_name, signature_def:"serving_default" - multiplier_2 = tf.constant(3.0, name="multiplier_2") - # tag:tag_name, signature_def:signature_def_name - multiplier_3 = tf.constant(4.0, name="multiplier_3") - - output_tensor_0 = tf.multiply(multiplier_0, input_tensor, name="TENSOR_OUTPUT") - output_tensor_1 = tf.multiply(multiplier_1, input_tensor, name="TENSOR_OUTPUT") - output_tensor_2 = tf.multiply(multiplier_2, input_tensor, name="TENSOR_OUTPUT") - output_tensor_3 = tf.multiply(multiplier_3, input_tensor, name="TENSOR_OUTPUT") - - # build_tensor_info_op could be used if build_tensor_info is deprecated - input_tensor_info = tf.saved_model.utils.build_tensor_info(input_tensor) - output_tensor_info_0 = tf.saved_model.utils.build_tensor_info(output_tensor_0) - output_tensor_info_1 = tf.saved_model.utils.build_tensor_info(output_tensor_1) - output_tensor_info_2 = tf.saved_model.utils.build_tensor_info(output_tensor_2) - output_tensor_info_3 = tf.saved_model.utils.build_tensor_info(output_tensor_3) - - # Using predict method name because simple save uses it - # tag:"serve", signature_def:"serving_default" - signature_0 = tf.saved_model.signature_def_utils.build_signature_def( - inputs={"INPUT": input_tensor_info}, - outputs={"OUTPUT": output_tensor_info_0}, - method_name=tf.saved_model.signature_constants.PREDICT_METHOD_NAME, - ) - # tag:"serve", signature_def:signature_def_name - signature_1 = tf.saved_model.signature_def_utils.build_signature_def( - inputs={"INPUT": input_tensor_info}, - outputs={"OUTPUT": output_tensor_info_1}, - method_name=tf.saved_model.signature_constants.PREDICT_METHOD_NAME, - ) - # tag:tag_name, signature_def:"serving_default" - signature_2 = tf.saved_model.signature_def_utils.build_signature_def( - inputs={"INPUT": input_tensor_info}, - outputs={"OUTPUT": output_tensor_info_2}, - method_name=tf.saved_model.signature_constants.PREDICT_METHOD_NAME, - ) - # tag:tag_name, signature_def:signature_def_name - signature_3 = tf.saved_model.signature_def_utils.build_signature_def( - inputs={"INPUT": input_tensor_info}, - outputs={"OUTPUT": output_tensor_info_3}, - method_name=tf.saved_model.signature_constants.PREDICT_METHOD_NAME, - ) - # tag:tag_name, signature_def:signature_def_name, two inputs/outputs - signature_4 = tf.saved_model.signature_def_utils.build_signature_def( - inputs={"INPUT": input_tensor_info, "INPUT1": input_tensor_info}, - outputs={"OUTPUT": output_tensor_info_0, "OUTPUT1": output_tensor_info_1}, - method_name=tf.saved_model.signature_constants.PREDICT_METHOD_NAME, - ) - - b = builder.SavedModelBuilder(model_version_dir + "/model.savedmodel") - - if different_io: - b.add_meta_graph_and_variables( - sess, - tags=[tag_name], - signature_def_map={signature_def_name: signature_0}, - assets_collection=ops.get_collection(ops.GraphKeys.ASSET_FILEPATHS), - clear_devices=True, - ) - b.add_meta_graph( - tags=[tag_constants.SERVING], - signature_def_map={ - signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY: signature_4 - }, - assets_collection=ops.get_collection(ops.GraphKeys.ASSET_FILEPATHS), - clear_devices=True, - ) - else: - signature_def_map_0 = { - signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY: signature_0, - signature_def_name: signature_1, - } - signature_def_map_1 = { - signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY: signature_2, - signature_def_name: signature_3, - } - - b.add_meta_graph_and_variables( - sess, - tags=[tag_constants.SERVING], - signature_def_map=signature_def_map_0, - assets_collection=ops.get_collection(ops.GraphKeys.ASSET_FILEPATHS), - clear_devices=True, - ) - b.add_meta_graph( - tags=[tag_name], - signature_def_map=signature_def_map_1, - assets_collection=ops.get_collection(ops.GraphKeys.ASSET_FILEPATHS), - clear_devices=True, - ) - - b.save() - - -def create_savedmodel_modelconfig( - models_dir, - model_version=1, - dims=16, - model_name="sig_tag", - tag_name="testTag", - signature_def_name="testSigDef", -): - config_dir = models_dir + "/" + model_name - config = """ -name: "{}" -platform: "tensorflow_savedmodel" -input [ - {{ - name: "INPUT" - data_type: {} - dims: [ {} ] - }} -] -output [ - {{ - name: "OUTPUT" - data_type: {} - dims: [ {} ] - }} -] -parameters: {{ -key: "TF_GRAPH_TAG" -value: {{ -string_value: "{}" -}} -}} -parameters: {{ -key: "TF_SIGNATURE_DEF" -value: {{ -string_value: "{}" -}} -}} -""".format( - model_name, - gu.np_to_model_dtype(tf.float32), - str(dims), - gu.np_to_model_dtype(tf.float32), - str(dims), - tag_name, - signature_def_name, - ) - - try: - os.makedirs(config_dir) - except OSError as ex: - pass # ignore existing dir - - with open(config_dir + "/config.pbtxt", "w") as cfile: - cfile.write(config) - - -if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser(description="getting model output dir") - parser.add_argument("--dir", help="directory to run model in", required=True) - args = parser.parse_args() - base_dir = args.dir - base_model_name = "sig_tag" - base_tag = "serve" - test_tag = "testTag" - base_sig_def = "serving_default" - test_sig_def = "testSigDef" - - for i in range(4): - model_name = base_model_name + str(i) - create_savedmodel( - base_dir, - model_name=model_name, - tag_name=test_tag, - signature_def_name=test_sig_def, - ) - create_savedmodel( - base_dir, - model_name=base_model_name + "_different_io", - tag_name=test_tag, - signature_def_name=test_sig_def, - different_io=True, - ) - create_savedmodel_modelconfig( - base_dir, - model_name="sig_tag0", - tag_name=base_tag, - signature_def_name=base_sig_def, - ) - create_savedmodel_modelconfig( - base_dir, - model_name="sig_tag1", - tag_name=base_tag, - signature_def_name=test_sig_def, - ) - create_savedmodel_modelconfig( - base_dir, - model_name="sig_tag2", - tag_name=test_tag, - signature_def_name=base_sig_def, - ) - create_savedmodel_modelconfig( - base_dir, - model_name="sig_tag3", - tag_name=test_tag, - signature_def_name=test_sig_def, - ) - create_savedmodel_modelconfig( - base_dir, - model_name="sig_tag_different_io", - tag_name=test_tag, - signature_def_name=test_sig_def, - ) diff --git a/qa/common/infer_test.py b/qa/common/infer_test.py index 21cf630e39..aa06197373 100755 --- a/qa/common/infer_test.py +++ b/qa/common/infer_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2020-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -40,7 +40,7 @@ np_dtype_string = np.dtype(object) # Allow caller to setup specific set of backends to test -DEFAULT_BACKENDS = "graphdef savedmodel plan onnx libtorch" +DEFAULT_BACKENDS = "plan onnx libtorch" TEST_BACKENDS = os.environ.get("BACKENDS", DEFAULT_BACKENDS).split() @@ -90,29 +90,6 @@ def _infer_exact_helper( input_size = 16 - if tu.validate_for_tf_model( - input_dtype, - output0_dtype, - output1_dtype, - (input_size,), - (input_size,), - (input_size,), - ): - for pf in ["graphdef", "savedmodel"]: - if pf in TEST_BACKENDS: - _infer_exact_helper( - self, - pf, - (input_size,), - 8, - input_dtype, - output0_dtype, - output1_dtype, - output0_raw=output0_raw, - output1_raw=output1_raw, - swap=swap, - ) - if tu.validate_for_trt_model( input_dtype, output0_dtype, diff --git a/qa/common/inferentia_perf_analyzer_input_data_json/simple_model.py b/qa/common/inferentia_perf_analyzer_input_data_json/simple_model.py index db7ca95848..e9d61c9b88 100755 --- a/qa/common/inferentia_perf_analyzer_input_data_json/simple_model.py +++ b/qa/common/inferentia_perf_analyzer_input_data_json/simple_model.py @@ -1,129 +1,83 @@ -#!/usr/bin/env python -# Copyright 2021-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import argparse - - -def gen_pytorch_model(name, batch_size): - class PyAddSubNet(nn.Module): - """ - Simple AddSub network in PyTorch. This network outputs the sum and - subtraction of the inputs. - """ - - def __init__(self): - super(PyAddSubNet, self).__init__() - - def forward(self, input0, input1): - return torch.sub(input0, input1, alpha=-1), torch.sub( - input0, input1, alpha=1 - ) - - model = PyAddSubNet() - model.eval() - batch_size = 1 - example_inputs = torch.zeros([8, 4], dtype=torch.int64), torch.zeros( - [8, 4], dtype=torch.int64 - ) - model_neuron = torch_neuron.trace(model, example_inputs, dynamic_batch_size=True) - model_neuron.save("{}.pt".format(name)) - - -def gen_tf_model(name, batch_size, tf_version): - # Set up model directory - model_dir = "add_sub_model" - compiled_model_dir = name - shutil.rmtree(model_dir, ignore_errors=True) - shutil.rmtree(compiled_model_dir, ignore_errors=True) - if tf_version == 1: - with tf.Session() as sess: - # Export SavedModel - input0 = tf.placeholder(tf.int64, [None, 4], "INPUT__0") - input1 = tf.placeholder(tf.int64, [None, 4], "INPUT__1") - output0 = tf.add(input0, input1, "OUTPUT__0") - output1 = tf.subtract(input0, input1, "OUTPUT__1") - tf.compat.v1.saved_model.simple_save( - session=sess, - export_dir=model_dir, - inputs={"INPUT__0": input0, "INPUT__1": input1}, - outputs={"OUTPUT__0": output0, "OUTPUT__1": output1}, - ) - # Compile using Neuron - tfn.saved_model.compile( - model_dir, - compiled_model_dir, - batch_size=batch_size, - dynamic_batch_size=True, - ) - elif tf_version == 2: - # TODO: Add gen scripts for TF2 - raise Exception("TensorFlow2 not yet supported") - else: - raise Exception("Unrecognized Tensorflow version: {}".format(tf_version)) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument( - "--model_type", - type=str, - required=True, - choices=["pytorch", "tensorflow"], - help="""The type of the compiled model. Currently, - only supports \"pytorch\" and \"tensorflow\".""", - ) - parser.add_argument( - "--name", type=str, required=True, help="The name of the compiled model" - ) - parser.add_argument( - "--tf_version", - type=int, - choices=[1, 2], - help="Version of tensorflow for compiled model", - ) - parser.add_argument( - "--batch_size", - type=int, - default=1, - help="The batch size for the compiled model", - ) - - FLAGS, unparsed = parser.parse_known_args() - if len(unparsed) > 0: - raise Exception("Unrecognized options: {}".format(unparsed)) - if FLAGS.model_type == "tensorflow": - import shutil - - import tensorflow as tf - import tensorflow.neuron as tfn - - gen_tf_model(FLAGS.name, FLAGS.batch_size, FLAGS.tf_version) - elif FLAGS.model_type == "pytorch": - import torch - import torch_neuron - from torch import nn - - gen_pytorch_model(FLAGS.name, FLAGS.batch_size) +#!/usr/bin/env python +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import argparse + + +def gen_pytorch_model(name, batch_size): + class PyAddSubNet(nn.Module): + """ + Simple AddSub network in PyTorch. This network outputs the sum and + subtraction of the inputs. + """ + + def __init__(self): + super(PyAddSubNet, self).__init__() + + def forward(self, input0, input1): + return torch.sub(input0, input1, alpha=-1), torch.sub( + input0, input1, alpha=1 + ) + + model = PyAddSubNet() + model.eval() + batch_size = 1 + example_inputs = torch.zeros([8, 4], dtype=torch.int64), torch.zeros( + [8, 4], dtype=torch.int64 + ) + model_neuron = torch_neuron.trace(model, example_inputs, dynamic_batch_size=True) + model_neuron.save("{}.pt".format(name)) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--model_type", + type=str, + required=True, + choices=["pytorch"], + help="""The type of the compiled model. Currently, + only supports \"pytorch\".""", + ) + parser.add_argument( + "--name", type=str, required=True, help="The name of the compiled model" + ) + parser.add_argument( + "--batch_size", + type=int, + default=1, + help="The batch size for the compiled model", + ) + + FLAGS, unparsed = parser.parse_known_args() + if len(unparsed) > 0: + raise Exception("Unrecognized options: {}".format(unparsed)) + elif FLAGS.model_type == "pytorch": + import torch + import torch_neuron + from torch import nn + + gen_pytorch_model(FLAGS.name, FLAGS.batch_size) diff --git a/qa/common/orca_header_test.py b/qa/common/orca_header_test.py new file mode 100755 index 0000000000..660b025556 --- /dev/null +++ b/qa/common/orca_header_test.py @@ -0,0 +1,167 @@ +#!/usr/bin/python3 +# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import sys + +sys.path.append("../common") + +import argparse +import json + +import requests + + +# To run the test, have tritonserver running and run this script with the endpoint as a flag. +# +# Example: +# ``` +# python3 orca_header_test.py http://localhost:8000/v2/models/ensemble/generate +# ``` +def get_endpoint_header(url, data, request_header=None): + """ + Sends a POST request to the given URL with the provided data and returns the value of the "endpoint-load-metrics" header, + or None if the request fails. + """ + HEADER_KEY = "endpoint-load-metrics" + try: + response = None + if request_header: + response = requests.post(url, json=data, headers=request_header) + else: + response = requests.post(url, json=data) + response.raise_for_status() + return response.headers.get(HEADER_KEY, "") + except requests.exceptions.RequestException as e: + print(f"Error making request: {e}") + return None + + +def parse_header_data(header, orca_format): + """ + Parses the header data into a dictionary based on the given format. + """ + METRIC_KEY = "named_metrics" + try: + if orca_format == "json": + # Parse the header in JSON format + data = json.loads(header.replace("JSON ", "")) + if METRIC_KEY in data: + return data[METRIC_KEY] + else: + print(f"No key '{METRIC_KEY}' in header data: {data}") + return None + elif orca_format == "text": + # Parse the header in TEXT format + data = {} + for key_value_pair in header.replace("TEXT ", "").split(", "): + key, value = key_value_pair.split("=") + if "." in key: + prefix, nested_key = key.split(".", 1) + if prefix == METRIC_KEY: + data[nested_key] = float(value) + if not data: + print(f"Could not parse any keys from header: {header}") + return None + return data + else: + print(f"Invalid ORCA format: {orca_format}") + return None + except (json.JSONDecodeError, ValueError, KeyError): + print("Error: Invalid data in the header.") + return None + + +def check_for_keys(data, desired_keys, orca_format): + """ + Checks if all desired keys are present in the given data dictionary. + """ + if all(key in data for key in desired_keys): + print( + "ORCA header present in ", + orca_format, + "format with" "kv_cache_utilization:", + [k + ": " + str(data[k]) for k in desired_keys], + ) + return True + else: + print(f"Missing keys in header: {', '.join(set(desired_keys) - set(data))}") + return False + + +def request_header(orca_format): + return {"endpoint-load-metrics-format": orca_format} if orca_format else None + + +def test_header_type(url, data, orca_format): + req_header = request_header(orca_format) + response_header = get_endpoint_header(args.url, TEST_DATA, req_header) + + desired_keys = { + "kv_cache_utilization", + "max_token_capacity", + } # Just the keys, no need to initialize with None + + if response_header is None: + print(f"Request to endpoint: '{args.url}' failed.") + return False + elif response_header == "": + if orca_format: + print( + f"response header empty, endpoint-load-metrics-format={orca_format} is not a valid ORCA metric format" + ) + return False + else: + # No request header set <=> no response header. Intended behavior. + print(f"response header empty, endpoint-load-metrics-format is not set") + return True + + data = parse_header_data(response_header, orca_format) + if data: + return check_for_keys(data, desired_keys, orca_format) + else: + print(f"Unexpected response header value: {response_header}") + return False + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Make a POST request to generate endpoint to test the ORCA metrics header." + ) + parser.add_argument("url", help="The model URL to send the request to.") + args = parser.parse_args() + TEST_DATA = json.loads( + '{"text_input": "hello world", "max_tokens": 20, "bad_words": "", "stop_words": ""}' + ) + passed = True + + for format in ["json", "text", None]: + print("Checking response header for ORCA format:", format) + if not test_header_type(args.url, TEST_DATA, format): + print("FAIL on format:", format) + passed = False + + sys.exit(0 if passed else 1) diff --git a/qa/common/sequence_util.py b/qa/common/sequence_util.py index 1b2560538d..0169369c37 100755 --- a/qa/common/sequence_util.py +++ b/qa/common/sequence_util.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -464,9 +464,7 @@ def check_sequence( """ if ( - ("savedmodel" not in trial) - and ("graphdef" not in trial) - and ("custom" not in trial) + ("custom" not in trial) and ("onnx" not in trial) and ("libtorch" not in trial) and ("plan" not in trial) @@ -754,9 +752,7 @@ def check_sequence_async( """ if ( - ("savedmodel" not in trial) - and ("graphdef" not in trial) - and ("custom" not in trial) + ("custom" not in trial) and ("onnx" not in trial) and ("libtorch" not in trial) and ("plan" not in trial) diff --git a/qa/common/test_util.py b/qa/common/test_util.py index d241f5909b..f3bbcdb16f 100755 --- a/qa/common/test_util.py +++ b/qa/common/test_util.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2018-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -54,10 +54,6 @@ def shape_is_fixed(shape): return shape_element_count(shape) != -1 -def shape_to_tf_shape(shape): - return [None if i == -1 else i for i in shape] - - def shape_to_onnx_shape(shape, idx=0, increment_index=True): # Onnx use string for variable size dimension, and the same string # will be inferred to have same value for the model run. @@ -78,31 +74,6 @@ def shape_to_dims_str(shape): return ",".join(str(i) for i in shape) -def validate_for_tf_model( - input_dtype, output0_dtype, output1_dtype, input_shape, output0_shape, output1_shape -): - """Return True if input and output dtypes are supported by a TF model.""" - - # Not extending test to uint8 yet - if ( - input_dtype == np.uint8 - or output0_dtype == np.uint8 - or output1_dtype == np.uint8 - ): - return False - - # If the input type is string the output type must be string or - # int32. This is because the QA models we generate convert strings - # internally to int32 for compute. - if (input_dtype == np.object_) and ( - ((output0_dtype != np.object_) and (output0_dtype != np.int32)) - or ((output1_dtype != np.object_) and (output1_dtype != np.int32)) - ): - return False - - return True - - def validate_for_trt_model( input_dtype, output0_dtype, output1_dtype, input_shape, output0_shape, output1_shape ): diff --git a/qa/ensemble_models/batch_to_nobatch_float32_float32_float32/config.pbtxt b/qa/ensemble_models/batch_to_nobatch_float32_float32_float32/config.pbtxt index 6e95e7bc0a..383f282663 100644 --- a/qa/ensemble_models/batch_to_nobatch_float32_float32_float32/config.pbtxt +++ b/qa/ensemble_models/batch_to_nobatch_float32_float32_float32/config.pbtxt @@ -1,4 +1,4 @@ -# Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -55,7 +55,7 @@ ensemble_scheduling { step [ { # batch model - model_name: "graphdef_float32_float32_float32" + model_name: "onnx_float32_float32_float32" model_version: 1 input_map { key: "INPUT0" diff --git a/qa/ensemble_models/batch_to_nobatch_nobatch_float32_float32_float32/config.pbtxt b/qa/ensemble_models/batch_to_nobatch_nobatch_float32_float32_float32/config.pbtxt index ea8d70c1ac..cbc79d1def 100644 --- a/qa/ensemble_models/batch_to_nobatch_nobatch_float32_float32_float32/config.pbtxt +++ b/qa/ensemble_models/batch_to_nobatch_nobatch_float32_float32_float32/config.pbtxt @@ -1,4 +1,4 @@ -# Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -55,7 +55,7 @@ ensemble_scheduling { step [ { # batch model - model_name: "graphdef_float32_float32_float32" + model_name: "onnx_float32_float32_float32" model_version: 1 input_map { key: "INPUT0" diff --git a/qa/ensemble_models/mix_ensemble_int32_float32_float32/config.pbtxt b/qa/ensemble_models/mix_ensemble_int32_float32_float32/config.pbtxt index b60a3db600..e4acd279e9 100644 --- a/qa/ensemble_models/mix_ensemble_int32_float32_float32/config.pbtxt +++ b/qa/ensemble_models/mix_ensemble_int32_float32_float32/config.pbtxt @@ -1,4 +1,4 @@ -# Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -54,7 +54,7 @@ output [ ensemble_scheduling { step [ { - model_name: "graphdef_int32_int32_int32" + model_name: "onnx_int32_int32_int32" model_version: 1 input_map { key: "INPUT0" @@ -70,7 +70,7 @@ ensemble_scheduling { } }, { - model_name: "graphdef_int32_object_object" + model_name: "onnx_int32_object_object" model_version: 1 input_map { key: "INPUT0" @@ -86,7 +86,7 @@ ensemble_scheduling { } }, { - model_name: "graphdef_int32_object_object" + model_name: "onnx_int32_object_object" model_version: 1 input_map { key: "INPUT0" @@ -102,7 +102,7 @@ ensemble_scheduling { } }, { - model_name: "graphdef_object_int32_int32" + model_name: "onnx_object_int32_int32" model_version: 1 input_map { key: "INPUT0" diff --git a/qa/ensemble_models/mix_nobatch_batch_float32_float32_float32/config.pbtxt b/qa/ensemble_models/mix_nobatch_batch_float32_float32_float32/config.pbtxt index 703b07a28e..a8f219fda7 100644 --- a/qa/ensemble_models/mix_nobatch_batch_float32_float32_float32/config.pbtxt +++ b/qa/ensemble_models/mix_nobatch_batch_float32_float32_float32/config.pbtxt @@ -1,4 +1,4 @@ -# Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -81,7 +81,7 @@ ensemble_scheduling { }, { # batch model - model_name: "graphdef_float32_float32_float32" + model_name: "onnx_float32_float32_float32" model_version: 1 input_map { key: "INPUT0" diff --git a/qa/ensemble_models/mix_platform_float32_float32_float32/config.pbtxt b/qa/ensemble_models/mix_platform_float32_float32_float32/config.pbtxt index 896d01fd89..c8a423a002 100644 --- a/qa/ensemble_models/mix_platform_float32_float32_float32/config.pbtxt +++ b/qa/ensemble_models/mix_platform_float32_float32_float32/config.pbtxt @@ -1,4 +1,4 @@ -# Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -54,7 +54,7 @@ output [ ensemble_scheduling { step [ { - model_name: "graphdef_float32_float32_float32" + model_name: "onnx_float32_float32_float32" model_version: 1 input_map { key: "INPUT0" @@ -70,7 +70,7 @@ ensemble_scheduling { } }, { - model_name: "savedmodel_float32_float32_float32" + model_name: "libtorch_float32_float32_float32" model_version: 1 input_map { key: "INPUT0" @@ -81,12 +81,12 @@ ensemble_scheduling { value: "INPUT1" } output_map { - key: "OUTPUT0" + key: "OUTPUT__0" value: "double_input1" } }, { - model_name: "savedmodel_float32_float32_float32" + model_name: "libtorch_float32_float32_float32" model_version: 1 input_map { key: "INPUT0" @@ -97,12 +97,12 @@ ensemble_scheduling { value: "INPUT0" } output_map { - key: "OUTPUT1" + key: "OUTPUT__1" value: "input0_val" } }, { - model_name: "graphdef_float32_float32_float32" + model_name: "onnx_float32_float32_float32" model_version: 1 input_map { key: "INPUT0" @@ -118,7 +118,7 @@ ensemble_scheduling { } }, { - model_name: "savedmodel_float32_float32_float32" + model_name: "libtorch_float32_float32_float32" model_version: 1 input_map { key: "INPUT0" @@ -129,11 +129,11 @@ ensemble_scheduling { value: "input1_val" } output_map { - key: "OUTPUT0" + key: "OUTPUT__0" value: "OUTPUT0" } output_map { - key: "OUTPUT1" + key: "OUTPUT__1" value: "OUTPUT1" } } diff --git a/qa/ensemble_models/mix_type_int32_float32_float32/config.pbtxt b/qa/ensemble_models/mix_type_int32_float32_float32/config.pbtxt index 125d0977c2..caae84fe6e 100644 --- a/qa/ensemble_models/mix_type_int32_float32_float32/config.pbtxt +++ b/qa/ensemble_models/mix_type_int32_float32_float32/config.pbtxt @@ -1,4 +1,4 @@ -# Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -54,7 +54,7 @@ output [ ensemble_scheduling { step [ { - model_name: "graphdef_int32_int32_int32" + model_name: "onnx_int32_int32_int32" model_version: 1 input_map { key: "INPUT0" @@ -70,7 +70,7 @@ ensemble_scheduling { } }, { - model_name: "graphdef_int32_object_object" + model_name: "onnx_int32_object_object" model_version: 1 input_map { key: "INPUT0" @@ -86,7 +86,7 @@ ensemble_scheduling { } }, { - model_name: "graphdef_int32_object_object" + model_name: "onnx_int32_object_object" model_version: 1 input_map { key: "INPUT0" @@ -102,7 +102,7 @@ ensemble_scheduling { } }, { - model_name: "graphdef_object_int32_int32" + model_name: "onnx_object_int32_int32" model_version: 1 input_map { key: "INPUT0" @@ -118,7 +118,7 @@ ensemble_scheduling { } }, { - model_name: "graphdef_int32_float32_float32" + model_name: "onnx_int32_float32_float32" model_version: 1 input_map { key: "INPUT0" @@ -134,7 +134,7 @@ ensemble_scheduling { } }, { - model_name: "graphdef_int32_float32_float32" + model_name: "onnx_int32_float32_float32" model_version: 1 input_map { key: "INPUT0" @@ -150,7 +150,7 @@ ensemble_scheduling { } }, { - model_name: "graphdef_float32_float32_float32" + model_name: "onnx_float32_float32_float32" model_version: 1 input_map { key: "INPUT0" diff --git a/qa/ensemble_models/nobatch_to_batch_float32_float32_float32/config.pbtxt b/qa/ensemble_models/nobatch_to_batch_float32_float32_float32/config.pbtxt index 85a78ea2a0..a5886c51c6 100644 --- a/qa/ensemble_models/nobatch_to_batch_float32_float32_float32/config.pbtxt +++ b/qa/ensemble_models/nobatch_to_batch_float32_float32_float32/config.pbtxt @@ -1,4 +1,4 @@ -# Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -81,7 +81,7 @@ ensemble_scheduling { }, { # batch model - model_name: "graphdef_float32_float32_float32" + model_name: "onnx_float32_float32_float32" model_version: 1 input_map { key: "INPUT0" diff --git a/qa/ensemble_models/nobatch_to_batch_nobatch_float32_float32_float32/config.pbtxt b/qa/ensemble_models/nobatch_to_batch_nobatch_float32_float32_float32/config.pbtxt index a791296255..02ab0d1b62 100644 --- a/qa/ensemble_models/nobatch_to_batch_nobatch_float32_float32_float32/config.pbtxt +++ b/qa/ensemble_models/nobatch_to_batch_nobatch_float32_float32_float32/config.pbtxt @@ -1,4 +1,4 @@ -# Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -81,7 +81,7 @@ ensemble_scheduling { }, { # batch model - model_name: "graphdef_float32_float32_float32" + model_name: "onnx_float32_float32_float32" model_version: 1 input_map { key: "INPUT0" diff --git a/qa/ensemble_models/wrong_label_int32_float32_float32/config.pbtxt b/qa/ensemble_models/wrong_label_int32_float32_float32/config.pbtxt index c262211a98..1e80730dc4 100644 --- a/qa/ensemble_models/wrong_label_int32_float32_float32/config.pbtxt +++ b/qa/ensemble_models/wrong_label_int32_float32_float32/config.pbtxt @@ -1,4 +1,4 @@ -# Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -55,7 +55,7 @@ output [ ensemble_scheduling { step [ { - model_name: "graphdef_int32_float32_float32" + model_name: "onnx_int32_float32_float32" model_version: 1 input_map { key: "INPUT0" diff --git a/qa/python_models/python_version/model.py b/qa/python_models/python_version/model.py index 5d77906fa9..b1157ea50d 100644 --- a/qa/python_models/python_version/model.py +++ b/qa/python_models/python_version/model.py @@ -1,4 +1,4 @@ -# Copyright 2021-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -45,16 +45,16 @@ def auto_complete_config(auto_complete_model_config): return auto_complete_model_config def initialize(self, args): - import tensorflow + import torch self.model_config = args["model_config"] # This is to make sure that /bin/bash is not picking up - # the wrong shared libraries after installing Tensorflow. + # the wrong shared libraries after installing PyTorch. # Tensorflow uses a shared library which is common with # bash. os.system("/bin/bash --help") print( - f"Python version is {sys.version_info.major}.{sys.version_info.minor}, NumPy version is {np.version.version}, and Tensorflow version is {tensorflow.__version__}", + f"Python version is {sys.version_info.major}.{sys.version_info.minor}, NumPy version is {np.version.version}, and PyTorch version is {torch.__version__}", flush=True, ) print(f"Locale is {locale.getlocale()}", flush=True) diff --git a/src/python/examples/example_model_repository/identity/1/model.onnx b/src/python/examples/example_model_repository/identity/1/model.onnx new file mode 100755 index 0000000000000000000000000000000000000000..eda3ec8d961a90f4b62fa0e7fde2cb9ee0fb4485 GIT binary patch literal 124 zcmdd w;|=5Uld@8iOQH}O1iAP*IE1*l7`WJlSj!TN;ti5yxL~FTVaOUfF$stQ0MSDn#Q*>R literal 0 HcmV?d00001 diff --git a/src/python/examples/example_model_repository/identity/1/model.savedmodel/saved_model.pb b/src/python/examples/example_model_repository/identity/1/model.savedmodel/saved_model.pb deleted file mode 100755 index 63f78fecb4d2e7a8fe857b668055ae7d7372c891..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 531 zcmb7>J5R$f5XW)Js~17hi2+%NArcaJNYesKR;Dh{A!UuC#-$OF6D1B+)x9I^d^Wxd zkSDMd;pKd~*YAI45PVVKm0$rj7?Y3-8Ob#QsF6quX%UZ_pe2Q@9jgz-Lq)Dr4!2-V z<6zF0NgVQw4!SfoEaTPz)So)tItDdg1}VqdP{r+?5KGN@_RN_!vPU=e)mSgYlSozq z-wA9$o7_!jo`XNx|_?>a|1-JuSzzez8l a-$4IWMXBT>PqIavtcDH9FwJ-v&E+Rf?!_4Z diff --git a/src/python/examples/example_model_repository/identity/config.pbtxt b/src/python/examples/example_model_repository/identity/config.pbtxt index ae83e47556..0ea4e80c26 100644 --- a/src/python/examples/example_model_repository/identity/config.pbtxt +++ b/src/python/examples/example_model_repository/identity/config.pbtxt @@ -1,4 +1,5 @@ -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -25,14 +26,18 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. name: "identity" -platform: "tensorflow_savedmodel" +platform: "onnxruntime_onnx" max_batch_size: 8 +version_policy: { latest { num_versions: 1 }} + + input [ { name: "INPUT0" data_type: TYPE_STRING dims: [ -1 ] + } ] output [ @@ -40,5 +45,7 @@ output [ name: "OUTPUT0" data_type: TYPE_STRING dims: [ -1 ] + + } ] diff --git a/src/python/setup.py b/src/python/setup.py index ee1e7c0ec4..067ff61035 100755 --- a/src/python/setup.py +++ b/src/python/setup.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -107,6 +107,6 @@ def get_tag(self): zip_safe=False, cmdclass={"bdist_wheel": bdist_wheel}, data_files=data_files, - install_requires=["tritonserver", "pydantic"], + install_requires=["tritonserver", "pydantic==2.10.6"], extras_require={"GPU": gpu_extras, "test": test_extras, "all": all_extras}, ) diff --git a/src/python/tritonfrontend/_api/_kservegrpc.py b/src/python/tritonfrontend/_api/_kservegrpc.py index efa706a77a..54959e1c89 100644 --- a/src/python/tritonfrontend/_api/_kservegrpc.py +++ b/src/python/tritonfrontend/_api/_kservegrpc.py @@ -1,4 +1,4 @@ -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -80,6 +80,7 @@ class Options: int, Grpc_compression_level ] = Grpc_compression_level.NONE infer_allocation_pool_size: int = Field(8, ge=0) + max_response_pool_size: int = Field(2_147_483_647, ge=0) forward_header_pattern: str = "" # DLIS-7215: Add restricted protocol support # restricted_protocols: str = "" diff --git a/src/python/tritonfrontend/_api/_kservegrpc.pyi b/src/python/tritonfrontend/_api/_kservegrpc.pyi index ae5d44ac96..0c8dc04804 100644 --- a/src/python/tritonfrontend/_api/_kservegrpc.pyi +++ b/src/python/tritonfrontend/_api/_kservegrpc.pyi @@ -1,4 +1,4 @@ -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -57,6 +57,7 @@ class KServeGrpc: max_connection_age_grace_ms: int infer_compression_level: int | Grpc_compression_level infer_allocation_pool_size: int + max_response_pool_size: int forward_header_pattern: str def __post_init__(self) -> None: ... triton_frontend: Incomplete From 6c6df119afe93e9ed03004433a674a6090ebda16 Mon Sep 17 00:00:00 2001 From: richardhuo-nv Date: Tue, 8 Apr 2025 13:33:45 -0700 Subject: [PATCH 011/230] test: Add tests cancelling BLS decoupled request in Python backend (#8097) --- .../decoupled/decoupled_test.py | 195 +++++++++++++++++- .../decoupled_bls_async_cancel/1/model.py | 93 +++++++++ .../decoupled_bls_async_cancel/config.pbtxt | 65 ++++++ .../models/decoupled_bls_cancel/1/model.py | 92 +++++++++ .../models/decoupled_bls_cancel/config.pbtxt | 65 ++++++ .../1/model.py | 103 +++++++++ .../config.pbtxt | 65 ++++++ qa/L0_backend_python/decoupled/test.sh | 6 +- .../config.pbtxt | 63 ++++++ .../response_sender_until_cancelled/model.py | 79 +++++++ 10 files changed, 824 insertions(+), 2 deletions(-) create mode 100644 qa/L0_backend_python/decoupled/models/decoupled_bls_async_cancel/1/model.py create mode 100644 qa/L0_backend_python/decoupled/models/decoupled_bls_async_cancel/config.pbtxt create mode 100644 qa/L0_backend_python/decoupled/models/decoupled_bls_cancel/1/model.py create mode 100644 qa/L0_backend_python/decoupled/models/decoupled_bls_cancel/config.pbtxt create mode 100644 qa/L0_backend_python/decoupled/models/decoupled_bls_cancel_after_complete/1/model.py create mode 100644 qa/L0_backend_python/decoupled/models/decoupled_bls_cancel_after_complete/config.pbtxt create mode 100644 qa/python_models/response_sender_until_cancelled/config.pbtxt create mode 100644 qa/python_models/response_sender_until_cancelled/model.py diff --git a/qa/L0_backend_python/decoupled/decoupled_test.py b/qa/L0_backend_python/decoupled/decoupled_test.py index 45ce370fb1..d3442e03a0 100755 --- a/qa/L0_backend_python/decoupled/decoupled_test.py +++ b/qa/L0_backend_python/decoupled/decoupled_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -46,6 +46,34 @@ _tritonserver_ipaddr = os.environ.get("TRITONSERVER_IPADDR", "localhost") +def prepare_decoupled_bls_cancel_inputs(input_value, max_sum_value, ignore_cancel): + input_data = np.array([input_value], dtype=np.int32) + max_sum_data = np.array([max_sum_value], dtype=np.int32) + ignore_cancel_data = np.array([ignore_cancel], dtype=np.bool_) + inputs = [ + grpcclient.InferInput( + "INPUT", + input_data.shape, + np_to_triton_dtype(input_data.dtype), + ), + grpcclient.InferInput( + "MAX_SUM", + max_sum_data.shape, + np_to_triton_dtype(max_sum_data.dtype), + ), + grpcclient.InferInput( + "IGNORE_CANCEL", + ignore_cancel_data.shape, + np_to_triton_dtype(ignore_cancel_data.dtype), + ), + ] + inputs[0].set_data_from_numpy(input_data) + inputs[1].set_data_from_numpy(max_sum_data) + inputs[2].set_data_from_numpy(ignore_cancel_data) + + return inputs + + class UserData: def __init__(self): self._completed_requests = queue.Queue() @@ -324,6 +352,171 @@ def test_decoupled_execute_cancel(self): self.assertIn("[execute_cancel] Request not cancelled at 1.0 s", log_text) self.assertIn("[execute_cancel] Request cancelled at ", log_text) + def test_decoupled_bls_cancel(self): + model_names = ["decoupled_bls_cancel", "decoupled_bls_async_cancel"] + input_value = 1 + max_sum_value = 10 + ignore_cancel = False + user_data = UserData() + for model_name in model_names: + with self._shm_leak_detector.Probe() as shm_probe: + with grpcclient.InferenceServerClient( + f"{_tritonserver_ipaddr}:8001" + ) as client: + client.start_stream(callback=partial(callback, user_data)) + inputs = prepare_decoupled_bls_cancel_inputs( + input_value=input_value, + max_sum_value=max_sum_value, + ignore_cancel=ignore_cancel, + ) + client.async_stream_infer(model_name, inputs) + + # Check the results of the decoupled model using BLS + def check_result(result): + # Make sure the result is not an exception + self.assertIsNot(type(result), InferenceServerException) + is_cancelled = result.as_numpy("IS_CANCELLED") + self.assertTrue( + is_cancelled[0], + "error: expected the request to be cancelled", + ) + + max_sum_data = np.array([max_sum_value], dtype=np.int32) + sum_data = result.as_numpy("SUM") + self.assertIsNotNone(sum_data, "error: expected 'SUM'") + self.assertTrue( + np.array_equal(sum_data, max_sum_data), + "error: expected output {} to match input {}".format( + sum_data, max_sum_data + ), + ) + + result = user_data._completed_requests.get() + check_result(result) + + def test_decoupled_bls_ignore_cancel(self): + model_names = ["decoupled_bls_cancel", "decoupled_bls_async_cancel"] + input_value = 1 + max_sum_value = 10 + ignore_cancel = True + user_data = UserData() + for model_name in model_names: + with self._shm_leak_detector.Probe() as shm_probe: + with grpcclient.InferenceServerClient( + f"{_tritonserver_ipaddr}:8001" + ) as client: + client.start_stream(callback=partial(callback, user_data)) + inputs = prepare_decoupled_bls_cancel_inputs( + input_value=input_value, + max_sum_value=max_sum_value, + ignore_cancel=ignore_cancel, + ) + client.async_stream_infer(model_name, inputs) + + # Check the results of the decoupled model using BLS + def check_result(result): + # Make sure the result is not an exception + self.assertIsNot(type(result), InferenceServerException) + is_cancelled = result.as_numpy("IS_CANCELLED") + self.assertFalse( + is_cancelled[0], + "error: expected the request not being cancelled", + ) + + max_sum_data = np.array([max_sum_value], dtype=np.int32) + sum_data = result.as_numpy("SUM") + self.assertIsNotNone(sum_data, "error: expected 'SUM'") + self.assertTrue( + sum_data > max_sum_data, + "error: expected sum_data {} to be greater than max_sum_data {}".format( + sum_data, max_sum_data + ), + ) + + result = user_data._completed_requests.get() + check_result(result) + + def test_decoupled_bls_cancel_after_cancellation(self): + model_name = "decoupled_bls_cancel_after_complete" + input_value = 1 + max_sum_value = 10 + ignore_cancel = False + user_data = UserData() + with self._shm_leak_detector.Probe() as shm_probe: + with grpcclient.InferenceServerClient( + f"{_tritonserver_ipaddr}:8001" + ) as client: + client.start_stream(callback=partial(callback, user_data)) + inputs = prepare_decoupled_bls_cancel_inputs( + input_value=input_value, + max_sum_value=max_sum_value, + ignore_cancel=ignore_cancel, + ) + client.async_stream_infer(model_name, inputs) + + # Check the results of the decoupled model using BLS + def check_result(result): + # Make sure the result is not an exception + self.assertIsNot(type(result), InferenceServerException) + is_cancelled = result.as_numpy("IS_CANCELLED") + self.assertTrue( + is_cancelled[0], "error: expected the request to be cancelled" + ) + + max_sum_data = np.array([max_sum_value], dtype=np.int32) + sum_data = result.as_numpy("SUM") + self.assertIsNotNone(sum_data, "error: expected 'SUM'") + self.assertTrue( + np.array_equal(sum_data, max_sum_data), + "error: expected output {} to match input {}".format( + sum_data, max_sum_data + ), + ) + + result = user_data._completed_requests.get() + check_result(result) + + def test_decoupled_bls_cancel_after_completion(self): + model_name = "decoupled_bls_cancel_after_complete" + input_value = 1 + max_sum_value = 25 + ignore_cancel = False + user_data = UserData() + with self._shm_leak_detector.Probe() as shm_probe: + with grpcclient.InferenceServerClient( + f"{_tritonserver_ipaddr}:8001" + ) as client: + client.start_stream(callback=partial(callback, user_data)) + inputs = prepare_decoupled_bls_cancel_inputs( + input_value=input_value, + max_sum_value=max_sum_value, + ignore_cancel=ignore_cancel, + ) + client.async_stream_infer(model_name, inputs) + + # Check the results of the decoupled model using BLS + def check_result(result): + # Make sure the result is not an exception + self.assertIsNot(type(result), InferenceServerException) + is_cancelled = result.as_numpy("IS_CANCELLED") + self.assertFalse( + is_cancelled[0], + "error: expected the request not being cancelled", + ) + + max_sum_data = np.array([max_sum_value], dtype=np.int32) + sum_data = result.as_numpy("SUM") + self.assertIsNotNone(sum_data, "error: expected 'SUM'") + self.assertTrue( + sum_data < max_sum_data, + "error: expected sum_data {} to be lesser than max_sum_data {}".format( + sum_data, max_sum_data + ), + ) + + result = user_data._completed_requests.get() + check_result(result) + def test_decoupled_raise_exception(self): # The decoupled_raise_exception model raises an exception for the request. # This test case is making sure that repeated exceptions are properly handled. diff --git a/qa/L0_backend_python/decoupled/models/decoupled_bls_async_cancel/1/model.py b/qa/L0_backend_python/decoupled/models/decoupled_bls_async_cancel/1/model.py new file mode 100644 index 0000000000..d84221e7e2 --- /dev/null +++ b/qa/L0_backend_python/decoupled/models/decoupled_bls_async_cancel/1/model.py @@ -0,0 +1,93 @@ +# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import asyncio + +import numpy as np +import triton_python_backend_utils as pb_utils + + +class TritonPythonModel: + """ + This model sends a decoupled bls inference request to 'response_sender_until_cancelled' + model, and sums up its responses. + Once the MAX_SUM is reached, the model will call the response iterator's + cancel() method to cancel the response stream. + If the IGNORE_CANCEL is set to True, the 'response_sender_until_cancelled' model will not hornor + the request cancellation and keep sending the output to the model. + The number of total responses should not reach MAX_RESPONSE_COUNT. + """ + + async def execute(self, requests): + max_sum = ( + pb_utils.get_input_tensor_by_name(requests[0], "MAX_SUM").as_numpy().flat[0] + ) + input = pb_utils.get_input_tensor_by_name(requests[0], "INPUT") + ignore_cancel = pb_utils.get_input_tensor_by_name(requests[0], "IGNORE_CANCEL") + delay = pb_utils.Tensor("DELAY", np.array([50], dtype=np.int32)) + max_response_count = pb_utils.Tensor( + "MAX_RESPONSE_COUNT", np.array([20], dtype=np.int32) + ) + + infer_request = pb_utils.InferenceRequest( + model_name="response_sender_until_cancelled", + inputs=[input, max_response_count, delay, ignore_cancel], + requested_output_names=["OUTPUT"], + ) + + response_stream = await infer_request.async_exec(decoupled=True) + + is_cancelled = False + error = None + response_sum = 0 + for infer_response in response_stream: + if infer_response.has_error(): + if infer_response.error().code() == pb_utils.TritonError.CANCELLED: + is_cancelled = True + else: + error = infer_response.error() + break + + out = pb_utils.get_output_tensor_by_name( + infer_response, "OUTPUT" + ).as_numpy()[0] + + response_sum += out + if response_sum >= max_sum: + response_stream.cancel() + + responses = [ + pb_utils.InferenceResponse( + output_tensors=[ + pb_utils.Tensor("SUM", np.array([response_sum], dtype=np.int32)), + pb_utils.Tensor( + "IS_CANCELLED", np.array([is_cancelled], dtype=np.bool_) + ), + ], + error=error, + ) + ] + + return responses diff --git a/qa/L0_backend_python/decoupled/models/decoupled_bls_async_cancel/config.pbtxt b/qa/L0_backend_python/decoupled/models/decoupled_bls_async_cancel/config.pbtxt new file mode 100644 index 0000000000..83cc39a6f6 --- /dev/null +++ b/qa/L0_backend_python/decoupled/models/decoupled_bls_async_cancel/config.pbtxt @@ -0,0 +1,65 @@ +# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +name: "decoupled_bls_async_cancel" +backend: "python" + +input [ + { + name: "INPUT" + data_type: TYPE_INT32 + dims: [ 1 ] + }, + { + name: "MAX_SUM" + data_type: TYPE_INT32 + dims: [ 1 ] + }, + { + name: "IGNORE_CANCEL" + data_type: TYPE_BOOL + dims: [ 1 ] + } +] +output [ + { + name: "SUM" + data_type: TYPE_INT32 + dims: [ 1 ] + }, + { + name: "IS_CANCELLED" + data_type: TYPE_BOOL + dims: [ 1 ] + } +] + +instance_group [ + { + count: 1 + kind : KIND_CPU + } +] diff --git a/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel/1/model.py b/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel/1/model.py new file mode 100644 index 0000000000..dab6414ede --- /dev/null +++ b/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel/1/model.py @@ -0,0 +1,92 @@ +# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import numpy as np +import triton_python_backend_utils as pb_utils + + +class TritonPythonModel: + """ + This model sends a decoupled bls inference request to 'response_sender_until_cancelled' + model, and sums up its responses. + Once the MAX_SUM is reached, the model will call the response iterator's + cancel() method to cancel the response stream. + If the IGNORE_CANCEL is set to True, the 'response_sender_until_cancelled' model will not hornor + the request cancellation and keep sending the output to the model. + The number of total responses should not reach MAX_RESPONSE_COUNT. + """ + + def execute(self, requests): + max_sum = ( + pb_utils.get_input_tensor_by_name(requests[0], "MAX_SUM").as_numpy().flat[0] + ) + input = pb_utils.get_input_tensor_by_name(requests[0], "INPUT") + ignore_cancel = pb_utils.get_input_tensor_by_name(requests[0], "IGNORE_CANCEL") + delay = pb_utils.Tensor("DELAY", np.array([50], dtype=np.int32)) + max_response_count = pb_utils.Tensor( + "MAX_RESPONSE_COUNT", np.array([20], dtype=np.int32) + ) + + infer_request = pb_utils.InferenceRequest( + model_name="response_sender_until_cancelled", + inputs=[input, max_response_count, delay, ignore_cancel], + requested_output_names=["OUTPUT"], + ) + + response_stream = infer_request.exec(decoupled=True) + + is_cancelled = False + error = None + response_sum = 0 + for infer_response in response_stream: + if infer_response.has_error(): + if infer_response.error().code() == pb_utils.TritonError.CANCELLED: + is_cancelled = True + else: + error = infer_response.error() + break + + out = pb_utils.get_output_tensor_by_name( + infer_response, "OUTPUT" + ).as_numpy()[0] + + response_sum += out + if response_sum >= max_sum: + response_stream.cancel() + + responses = [ + pb_utils.InferenceResponse( + output_tensors=[ + pb_utils.Tensor("SUM", np.array([response_sum], dtype=np.int32)), + pb_utils.Tensor( + "IS_CANCELLED", np.array([is_cancelled], dtype=np.bool_) + ), + ], + error=error, + ) + ] + + return responses diff --git a/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel/config.pbtxt b/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel/config.pbtxt new file mode 100644 index 0000000000..0b443de6f9 --- /dev/null +++ b/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel/config.pbtxt @@ -0,0 +1,65 @@ +# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +name: "decoupled_bls_cancel" +backend: "python" + +input [ + { + name: "INPUT" + data_type: TYPE_INT32 + dims: [ 1 ] + }, + { + name: "MAX_SUM" + data_type: TYPE_INT32 + dims: [ 1 ] + }, + { + name: "IGNORE_CANCEL" + data_type: TYPE_BOOL + dims: [ 1 ] + } +] +output [ + { + name: "SUM" + data_type: TYPE_INT32 + dims: [ 1 ] + }, + { + name: "IS_CANCELLED" + data_type: TYPE_BOOL + dims: [ 1 ] + } +] + +instance_group [ + { + count: 1 + kind : KIND_CPU + } +] diff --git a/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel_after_complete/1/model.py b/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel_after_complete/1/model.py new file mode 100644 index 0000000000..4c4202de1a --- /dev/null +++ b/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel_after_complete/1/model.py @@ -0,0 +1,103 @@ +# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import asyncio + +import numpy as np +import triton_python_backend_utils as pb_utils + + +class TritonPythonModel: + """ + This model sends a decoupled bls inference request to 'response_sender_until_cancelled' + model, and sums up its responses. + Once the MAX_SUM is reached, the model will call the response iterator's + cancel() method to cancel the response stream. + If the IGNORE_CANCEL is set to True, the 'response_sender_until_cancelled' model will not hornor + the request cancellation and keep sending the output to the model. + The number of total responses should not reach MAX_RESPONSE_COUNT. + """ + + async def execute(self, requests): + max_sum = ( + pb_utils.get_input_tensor_by_name(requests[0], "MAX_SUM").as_numpy().flat[0] + ) + input = pb_utils.get_input_tensor_by_name(requests[0], "INPUT") + ignore_cancel = pb_utils.get_input_tensor_by_name(requests[0], "IGNORE_CANCEL") + delay = pb_utils.Tensor("DELAY", np.array([50], dtype=np.int32)) + max_response_count = pb_utils.Tensor( + "MAX_RESPONSE_COUNT", np.array([20], dtype=np.int32) + ) + + infer_request = pb_utils.InferenceRequest( + model_name="response_sender_until_cancelled", + inputs=[input, max_response_count, delay, ignore_cancel], + requested_output_names=["OUTPUT"], + ) + + response_stream = await infer_request.async_exec(decoupled=True) + + is_cancelled = False + error = None + response_sum = 0 + for infer_response in response_stream: + if infer_response.has_error(): + if infer_response.error().code() == pb_utils.TritonError.CANCELLED: + is_cancelled = True + else: + error = infer_response.error() + break + + out = pb_utils.get_output_tensor_by_name( + infer_response, "OUTPUT" + ).as_numpy()[0] + + response_sum += out + if response_sum >= max_sum: + response_stream.cancel() + + # test cancel after request completion. + if not error: + try: + response_stream.cancel() + except Exception as e: + error = pb_utils.TritonError( + message=str(e), + code=pb_utils.TritonError.INTERNAL, + ) + + responses = [ + pb_utils.InferenceResponse( + output_tensors=[ + pb_utils.Tensor("SUM", np.array([response_sum], dtype=np.int32)), + pb_utils.Tensor( + "IS_CANCELLED", np.array([is_cancelled], dtype=np.bool_) + ), + ], + error=error, + ) + ] + + return responses diff --git a/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel_after_complete/config.pbtxt b/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel_after_complete/config.pbtxt new file mode 100644 index 0000000000..cdbffa5419 --- /dev/null +++ b/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel_after_complete/config.pbtxt @@ -0,0 +1,65 @@ +# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +name: "decoupled_bls_cancel_after_complete" +backend: "python" + +input [ + { + name: "INPUT" + data_type: TYPE_INT32 + dims: [ 1 ] + }, + { + name: "MAX_SUM" + data_type: TYPE_INT32 + dims: [ 1 ] + }, + { + name: "IGNORE_CANCEL" + data_type: TYPE_BOOL + dims: [ 1 ] + } +] +output [ + { + name: "SUM" + data_type: TYPE_INT32 + dims: [ 1 ] + }, + { + name: "IS_CANCELLED" + data_type: TYPE_BOOL + dims: [ 1 ] + } +] + +instance_group [ + { + count: 1 + kind : KIND_CPU + } +] diff --git a/qa/L0_backend_python/decoupled/test.sh b/qa/L0_backend_python/decoupled/test.sh index 672335c892..622c151050 100755 --- a/qa/L0_backend_python/decoupled/test.sh +++ b/qa/L0_backend_python/decoupled/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -53,6 +53,10 @@ cp ../../python_models/execute_cancel/model.py ./models/execute_cancel/1/ cp ../../python_models/execute_cancel/config.pbtxt ./models/execute_cancel/ echo "model_transaction_policy { decoupled: True }" >> ./models/execute_cancel/config.pbtxt +mkdir -p models/response_sender_until_cancelled/1/ +cp ../../python_models/response_sender_until_cancelled/model.py ./models/response_sender_until_cancelled/1/ +cp ../../python_models/response_sender_until_cancelled/config.pbtxt ./models/response_sender_until_cancelled/ + rm -fr python_backend git clone ${TRITON_REPO_ORGANIZATION}/python_backend -b $PYTHON_BACKEND_REPO_TAG mkdir -p models/square_int32/1/ diff --git a/qa/python_models/response_sender_until_cancelled/config.pbtxt b/qa/python_models/response_sender_until_cancelled/config.pbtxt new file mode 100644 index 0000000000..34a703db39 --- /dev/null +++ b/qa/python_models/response_sender_until_cancelled/config.pbtxt @@ -0,0 +1,63 @@ +# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +name: "response_sender_until_cancelled" +backend: "python" +model_transaction_policy { + decoupled: True +} + +input [ + { + name: "MAX_RESPONSE_COUNT" + data_type: TYPE_INT32 + dims: [ 1 ] + }, + { + name: "DELAY" + data_type: TYPE_INT32 + dims: [ 1 ] + }, + { + name: "INPUT" + data_type: TYPE_INT32 + dims: [ 1 ] + }, + { + name: "IGNORE_CANCEL" + data_type: TYPE_BOOL + dims: [ 1 ] + } +] +output [ + { + name: "OUTPUT" + data_type: TYPE_INT32 + dims: [ 1 ] + } +] + +instance_group [{ kind: KIND_CPU }] diff --git a/qa/python_models/response_sender_until_cancelled/model.py b/qa/python_models/response_sender_until_cancelled/model.py new file mode 100644 index 0000000000..c4ffe8dd32 --- /dev/null +++ b/qa/python_models/response_sender_until_cancelled/model.py @@ -0,0 +1,79 @@ +# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import time + +import numpy as np +import triton_python_backend_utils as pb_utils + + +class TritonPythonModel: + """This model will keep repeating the INPUT as the OUTPUT, + until the request is being cancelled or + the MAX_RESPONSE_COUNT has been reached. + """ + + def execute(self, requests): + request = requests[0] + + input = pb_utils.get_input_tensor_by_name(request, "INPUT").as_numpy() + max_response_count = pb_utils.get_input_tensor_by_name( + request, "MAX_RESPONSE_COUNT" + ).as_numpy()[0] + delay = pb_utils.get_input_tensor_by_name(request, "DELAY").as_numpy()[0] + ignore_cancel = pb_utils.get_input_tensor_by_name( + request, "IGNORE_CANCEL" + ).as_numpy()[0] + response_sender = request.get_response_sender() + + sent = 0 + while True: + if not ignore_cancel and request.is_cancelled(): + response = pb_utils.InferenceResponse( + error=pb_utils.TritonError( + message="request has been cancelled", + code=pb_utils.TritonError.CANCELLED, + ) + ) + response_sender.send( + response, flags=pb_utils.TRITONSERVER_RESPONSE_COMPLETE_FINAL + ) + break + + output = pb_utils.Tensor("OUTPUT", np.array([input[0]], np.int32)) + response = pb_utils.InferenceResponse(output_tensors=[output]) + + if sent + 1 == max_response_count: + response_sender.send( + response, flags=pb_utils.TRITONSERVER_RESPONSE_COMPLETE_FINAL + ) + break + else: + response_sender.send(response) + sent += 1 + time.sleep(delay / 1000) + + return None From ef52c84a558cfec7aa1d38032f57001b2d2fe8c2 Mon Sep 17 00:00:00 2001 From: Olga Andreeva <124622579+oandreeva-nv@users.noreply.github.com> Date: Fri, 11 Apr 2025 16:36:21 -0500 Subject: [PATCH 012/230] vLLM backend SBSA build (#8142) --- build.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/build.py b/build.py index 9b24e50fc6..87bd3d7030 100755 --- a/build.py +++ b/build.py @@ -1484,13 +1484,24 @@ def dockerfile_prepare_container_linux(argmap, backends, enable_gpu, target_mach ARG BUILD_PUBLIC_VLLM="true" ARG VLLM_INDEX_URL ARG PYTORCH_TRITON_URL +ARG NVPL_SLIM_URL RUN --mount=type=secret,id=req,target=/run/secrets/requirements \\ if [ "$BUILD_PUBLIC_VLLM" = "false" ]; then \\ - pip3 install --no-cache-dir \\ - mkl==2021.1.1 \\ - mkl-include==2021.1.1 \\ - mkl-devel==2021.1.1 \\ + if [ "$(uname -m)" = "x86_64" ]; then \\ + pip3 install --no-cache-dir \\ + mkl==2021.1.1 \\ + mkl-include==2021.1.1 \\ + mkl-devel==2021.1.1; \\ + elif [ "$(uname -m)" = "aarch64" ]; then \\ + echo "Downloading NVPL from: $NVPL_SLIM_URL" && \\ + cd /tmp && \\ + wget -O nvpl_slim_24.04.tar $NVPL_SLIM_URL && \\ + tar -xf nvpl_slim_24.04.tar && \\ + cp -r nvpl_slim_24.04/lib/* /usr/local/lib && \\ + cp -r nvpl_slim_24.04/include/* /usr/local/include && \\ + rm -rf nvpl_slim_24.04.tar nvpl_slim_24.04; \\ + fi \\ && pip3 install --no-cache-dir --progress-bar on --index-url $VLLM_INDEX_URL -r /run/secrets/requirements \\ # Need to install in-house build of pytorch-triton to support triton_key definition used by torch 2.5.1 && cd /tmp \\ @@ -1893,6 +1904,7 @@ def create_docker_build_script(script_name, container_install_dir, container_ci_ f"--build-arg VLLM_INDEX_URL={vllm_index_url}", f"--build-arg PYTORCH_TRITON_URL={pytorch_triton_url}", f"--build-arg BUILD_PUBLIC_VLLM={build_public_vllm}", + f"--build-arg NVPL_SLIM_URL={nvpl_slim_url}", ] finalargs += [ "-t", @@ -2883,6 +2895,7 @@ def enable_all(): requirements = secrets.get("req", "") vllm_index_url = secrets.get("vllm_index_url", "") pytorch_triton_url = secrets.get("pytorch_triton_url", "") + nvpl_slim_url = secrets.get("nvpl_slim_url", "") build_public_vllm = secrets.get("build_public_vllm", "true") log('Build Arg for BUILD_PUBLIC_VLLM: "{}"'.format(build_public_vllm)) From 25baa7b33e68ba5a1220433c70d447777ef91288 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Mon, 14 Apr 2025 12:47:23 -0400 Subject: [PATCH 013/230] fix: Fix segfaults in tracing mode after long run (#8144) --- qa/L0_trace/test.sh | 99 +++++++++++++++++++++++++ qa/L0_trace/trace_stress_grpc_client.py | 90 ++++++++++++++++++++++ src/tracer.cc | 31 +++++--- 3 files changed, 210 insertions(+), 10 deletions(-) create mode 100755 qa/L0_trace/trace_stress_grpc_client.py diff --git a/qa/L0_trace/test.sh b/qa/L0_trace/test.sh index ce53ea8528..8322e28d68 100755 --- a/qa/L0_trace/test.sh +++ b/qa/L0_trace/test.sh @@ -109,6 +109,11 @@ mkdir -p $MODELSDIR/custom_identity_int32/1 && (cd $MODELSDIR/custom_identity_in RET=0 +# set up identity_fp32 model +mkdir -p $MODELSDIR/identity_fp32/1 && \ + cp ../python_models/identity_fp32/model.py $MODELSDIR/identity_fp32/1/. && \ + cp ../python_models/identity_fp32/config.pbtxt $MODELSDIR/identity_fp32/. + # Helpers ======================================= function assert_curl_success { message="${1}" @@ -186,6 +191,26 @@ function send_inference_requests { done } +function run_stress_client { + stress_client="${1}" + client_log="${2}" + echo "Running stress test for 120 seconds..." + bash -c ' + # Handle SIGTERM (signal 15) and exit gracefully + trap "echo \"cleaning up stress client...\"; exit 0" SIGTERM + + while true; do + python3 "$1" >> "$2" + sleep 0.1 + done' _ "$stress_client" "$client_log" & CLIENT_PID=$! + sleep 120 + + set -e + kill $CLIENT_PID + wait $CLIENT_PID + set +e +} + #======================================= # start with trace-level=OFF @@ -1244,4 +1269,78 @@ set -e kill $SERVER_PID wait $SERVER_PID set +e + +# Long running stress test +# Triton trace mode +SERVER_ARGS="--model-control-mode=explicit \ + --model-repository=$MODELSDIR \ + --load-model=identity_fp32 \ + --trace-config mode=triton \ + --trace-config triton,file=./trace \ + --trace-config rate=1 \ + --trace-config level=TIMESTAMPS" +SERVER_LOG="./inference_server_triton_trace_stress.log" +CLIENT_LOG="./client_triton_trace_stress.log" +STRESS_CLIENT="./trace_stress_grpc_client.py" + +run_server +if [ "$SERVER_PID" == "0" ]; then + echo -e "\n***\n*** Failed to start $SERVER\n***" + cat $SERVER_LOG + exit 1 +fi + +# Run stress test +run_stress_client $STRESS_CLIENT $CLIENT_LOG + +set -e +if ! kill -0 ${SERVER_PID} > /dev/null 2>&1; then + echo -e "\n***\n*** Server stopped unexpectedly during stress test\n***" + cat $SERVER_LOG + RET=1 +else + kill $SERVER_PID + wait $SERVER_PID +fi +set +e + +# Opentelemetry trace mode +SERVER_ARGS="--model-control-mode=explicit \ + --model-repository=$MODELSDIR \ + --load-model=identity_fp32 \ + --trace-config level=TIMESTAMPS \ + --trace-config rate=1 \ + --trace-config mode=opentelemetry \ + --trace-config opentelemetry,resource=test.key=test.value \ + --trace-config opentelemetry,resource=service.name=test_triton \ + --trace-config opentelemetry,url=localhost:$OTLP_PORT/v1/traces" +SERVER_LOG="./inference_server_otel_trace_stress.log" +CLIENT_LOG="./client_otel_trace_stress.log" +STRESS_CLIENT="./trace_stress_grpc_client.py" + +run_server +if [ "$SERVER_PID" == "0" ]; then + echo -e "\n***\n*** Failed to start $SERVER\n***" + cat $SERVER_LOG + exit 1 +fi + +rm collected_traces.json +$OTEL_COLLECTOR --config ./trace-config.yaml >> $OTEL_COLLECTOR_LOG 2>&1 & COLLECTOR_PID=$! +# Run stress test +run_stress_client $STRESS_CLIENT $CLIENT_LOG + +set -e +kill $COLLECTOR_PID +wait $COLLECTOR_PID +if ! kill -0 ${SERVER_PID} > /dev/null 2>&1; then + echo -e "\n***\n*** Server stopped unexpectedly during stress test\n***" + cat $SERVER_LOG + RET=1 +else + kill $SERVER_PID + wait $SERVER_PID +fi +set +e + exit $RET diff --git a/qa/L0_trace/trace_stress_grpc_client.py b/qa/L0_trace/trace_stress_grpc_client.py new file mode 100755 index 0000000000..99a676f150 --- /dev/null +++ b/qa/L0_trace/trace_stress_grpc_client.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import random +import sys +import time +from functools import partial + +import numpy as np +import tritonclient.grpc as grpcclient + +if __name__ == "__main__": + # 1 ms cancellation timeout + client_timeout = 1 + url = "localhost:8001" + + try: + triton_client = grpcclient.InferenceServerClient(url=url) + except Exception as e: + print("context creation failed: " + str(e)) + sys.exit() + + model_name = "identity_fp32" + + # Infer + inputs = [] + + input_data = np.array( + [random.random() for i in range(50)], dtype=np.float32 + ).reshape(1, -1) + model_input = grpcclient.InferInput( + name="INPUT0", datatype="FP32", shape=input_data.shape + ) + model_input.set_data_from_numpy(input_data) + inputs.append(model_input) + + # Define the callback function. Note the last two parameters should be + # result and error. InferenceServerClient would povide the results of an + # inference as grpcclient.InferResult in result. For successful + # inference, error will be None, otherwise it will be an object of + # tritonclientutils.InferenceServerException holding the error details + def callback(user_data, result, error): + if error: + user_data.append(error) + else: + user_data.append(result) + + # list to hold the results of inference. + user_data = [] + + # Inference call + for _ in range(1000): + triton_client.async_infer( + model_name=model_name, + inputs=inputs, + callback=partial(callback, user_data), + client_timeout=client_timeout, + ) + + # Wait until the results are available in user_data + time_out = 20 + while (len(user_data) == 0) and time_out > 0: + time_out = time_out - 1 + time.sleep(1) + + print("results: ", len(user_data)) diff --git a/src/tracer.cc b/src/tracer.cc index 5557106dfd..7955cfe3bc 100644 --- a/src/tracer.cc +++ b/src/tracer.cc @@ -1,4 +1,4 @@ -// Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -740,16 +740,27 @@ TraceManager::TraceRelease(TRITONSERVER_InferenceTrace* trace, void* userp) LOG_TRITONSERVER_ERROR( TRITONSERVER_InferenceTraceId(trace, &id), "getting trace id"); - auto ts = reinterpret_cast*>(userp); - std::lock_guard lk((*ts)->mtx_); - (*ts)->spawned_traces_tracker_.erase(id); - // The userp will be shared with the trace children, so only delete it - // if no more TraceRelease calls are expected - if ((*ts)->spawned_traces_tracker_.empty()) { - delete ts; + auto ts_ptr = reinterpret_cast*>(userp); + std::shared_ptr tracer_sp; + bool delete_ts = false; + { + std::lock_guard lk((*ts_ptr)->mtx_); + (*ts_ptr)->spawned_traces_tracker_.erase(id); + // The userp will be shared with the trace children, so only delete it + // if no more TraceRelease calls are expected + if ((*ts_ptr)->spawned_traces_tracker_.empty()) { + // Move the trace shared_ptr out inside lock to ensure mutex stays alive + // and destruct outside lock + tracer_sp = std::move(*ts_ptr); + delete_ts = true; + delete ts_ptr; + } + LOG_TRITONSERVER_ERROR( + TRITONSERVER_InferenceTraceDelete(trace), "deleting trace"); + } + if (delete_ts) { + tracer_sp.reset(); } - LOG_TRITONSERVER_ERROR( - TRITONSERVER_InferenceTraceDelete(trace), "deleting trace"); } const char* From 1e7ba25a9b0aa0f9de9c81c40845453796b9dbbc Mon Sep 17 00:00:00 2001 From: Dmitry Tokarev Date: Mon, 14 Apr 2025 12:51:58 -0400 Subject: [PATCH 014/230] ci: Removed obsolete lib: libnvToolsExt.so (#8146) --- build.py | 1 - 1 file changed, 1 deletion(-) diff --git a/build.py b/build.py index 87bd3d7030..8d66b68027 100755 --- a/build.py +++ b/build.py @@ -1572,7 +1572,6 @@ def add_cpu_libs_to_linux_dockerfile(backends, target_machine): RUN mkdir -p /usr/local/cuda/targets/{cuda_arch}-linux/lib COPY --from=min_container /usr/local/cuda/lib64/libcudart.so.12 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. COPY --from=min_container /usr/local/cuda/lib64/libcupti.so.12 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. -COPY --from=min_container /usr/local/cuda/lib64/libnvToolsExt.so.1 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. COPY --from=min_container /usr/local/cuda/lib64/libnvJitLink.so.12 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. RUN mkdir -p /opt/hpcx/ucc/lib/ /opt/hpcx/ucx/lib/ From 27f0410b8d37dd8fca35204761d3d9fda867e490 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Mon, 14 Apr 2025 15:17:03 -0700 Subject: [PATCH 015/230] fix: Update REAMDE.md (#8148) --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 3ec027aa6c..63c67db1a1 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,12 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --> +[![License](https://img.shields.io/badge/License-BSD3-lightgrey.svg)](https://opensource.org/licenses/BSD-3-Clause) + +>[!WARNING] +>You are currently on the `main` branch which tracks under-development progress +>towards the next release. The current release is version [2.56.0](https://github.com/triton-inference-server/server/releases/latest) +>and corresponds to the 25.03 container release on NVIDIA GPU Cloud (NGC). # Triton Inference Server From 5eb09ce9428eb55c042b4eaccc945ec4aad118db Mon Sep 17 00:00:00 2001 From: Jacky <18255193+kthui@users.noreply.github.com> Date: Mon, 14 Apr 2025 16:14:09 -0700 Subject: [PATCH 016/230] feat: Add multi-LoRA support to OpenAI frontend (#8038) --- python/openai/README.md | 63 ++++ .../openai_frontend/engine/triton_engine.py | 101 ++++- .../openai_frontend/engine/utils/triton.py | 70 +++- python/openai/openai_frontend/main.py | 13 +- python/openai/tests/test_lora.py | 353 ++++++++++++++++++ python/openai/tests/utils.py | 4 +- 6 files changed, 576 insertions(+), 28 deletions(-) create mode 100644 python/openai/tests/test_lora.py diff --git a/python/openai/README.md b/python/openai/README.md index 53a9f461f4..ff642555b5 100644 --- a/python/openai/README.md +++ b/python/openai/README.md @@ -230,6 +230,69 @@ pip install -r requirements-test.txt pytest -v tests/ ``` +### LoRA Adapters + +If the command line argument `--lora-separator=` is provided +when starting the OpenAI Frontend, a vLLM LoRA adaptor listed on the +`multi_lora.json` may be selected by appending the LoRA name to the model name, +separated by the LoRA separator, on the inference request in +`` format. + +
+For example + +```bash +# start server with model named gemma-2b +python3 openai_frontend/main.py --lora-separator=_lora_ ... + +# inference without LoRA +curl -s http://localhost:9000/v1/completions -H 'Content-Type: application/json' -d '{ + "model": "gemma-2b", + "temperature": 0, + "prompt": "When was the wheel invented?" +}' +{ + ... + "choices":[{..."text":"\n\nThe wheel was invented by the Sumerians in Mesopotamia around 350"}], + ... +} + +# inference with LoRA named doll +curl -s http://localhost:9000/v1/completions -H 'Content-Type: application/json' -d '{ + "model": "gemma-2b_lora_doll", + "temperature": 0, + "prompt": "When was the wheel invented?" +}' +{ + ... + "choices":[{..."text":"\n\nThe wheel was invented in Mesopotamia around 3500 BC.\n\n"}], + ... +} + +# inference with LoRA named sheep +curl -s http://localhost:9000/v1/completions -H 'Content-Type: application/json' -d '{ + "model": "gemma-2b_lora_sheep", + "temperature": 0, + "prompt": "When was the wheel invented?" +}' +{ + ... + "choices":[{..."text":"\n\nThe wheel was invented around 3000 BC in Mesopotamia.\n\n"}], + ... +} +``` + +
+ +When listing or retrieving model(s), the model id will include the LoRA name in +the same `` format for each LoRA +adapter listed on the `multi_lora.json`. Note: The LoRA name inclusion is +limited to locally stored models, inference requests are not limited though. + +See the +[vLLM documentation](https://github.com/triton-inference-server/vllm_backend/blob/main/docs/llama_multi_lora_tutorial.md) +on how to serve a model with LoRA adapters. + ## TensorRT-LLM 0. Prepare your model repository for a TensorRT-LLM model, build the engine, etc. You can try any of the following options: diff --git a/python/openai/openai_frontend/engine/triton_engine.py b/python/openai/openai_frontend/engine/triton_engine.py index e315cbbfd8..de6c1ed2cf 100644 --- a/python/openai/openai_frontend/engine/triton_engine.py +++ b/python/openai/openai_frontend/engine/triton_engine.py @@ -1,4 +1,4 @@ -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -39,6 +39,7 @@ _create_trtllm_inference_request, _create_vllm_inference_request, _get_output, + _get_vllm_lora_names, _validate_triton_responses_non_streaming, ) from schemas.openai import ( @@ -70,6 +71,8 @@ class TritonModelMetadata: model: tritonserver.Model # Tokenizers used for chat templates tokenizer: Optional[Any] + # LoRA names supported by the backend + lora_names: Optional[List[str]] # Time that model was loaded by Triton create_time: int # Conversion format between OpenAI and Triton requests @@ -78,13 +81,18 @@ class TritonModelMetadata: class TritonLLMEngine(LLMEngine): def __init__( - self, server: tritonserver.Server, tokenizer: str, backend: Optional[str] = None + self, + server: tritonserver.Server, + tokenizer: str, + backend: Optional[str] = None, + lora_separator: Optional[str] = None, ): # Assume an already configured and started server self.server = server self.tokenizer = self._get_tokenizer(tokenizer) # TODO: Reconsider name of "backend" vs. something like "request_format" self.backend = backend + self.lora_separator = lora_separator # NOTE: Creation time and model metadata will be static at startup for # now, and won't account for dynamically loading/unloading models. @@ -100,22 +108,35 @@ def metrics(self) -> str: def models(self) -> List[Model]: models = [] for metadata in self.model_metadata.values(): - models.append( - Model( - id=metadata.name, - created=metadata.create_time, - object=ObjectType.model, - owned_by="Triton Inference Server", - ), - ) + model_names = [metadata.name] + if ( + self.lora_separator is not None + and len(self.lora_separator) > 0 + and metadata.lora_names is not None + ): + for lora_name in metadata.lora_names: + model_names.append( + f"{metadata.name}{self.lora_separator}{lora_name}" + ) + + for model_name in model_names: + models.append( + Model( + id=model_name, + created=metadata.create_time, + object=ObjectType.model, + owned_by="Triton Inference Server", + ), + ) return models async def chat( self, request: CreateChatCompletionRequest ) -> CreateChatCompletionResponse | AsyncIterator[str]: - metadata = self.model_metadata.get(request.model) - self._validate_chat_request(request, metadata) + model_name, lora_name = self._get_model_and_lora_name(request.model) + metadata = self.model_metadata.get(model_name) + self._validate_chat_request(request, metadata, lora_name) conversation = [ message.model_dump(exclude_none=True) for message in request.messages @@ -130,7 +151,7 @@ async def chat( # Convert to Triton request format and perform inference responses = metadata.model.async_infer( - metadata.request_converter(metadata.model, prompt, request) + metadata.request_converter(metadata.model, prompt, request, lora_name) ) # Prepare and send responses back to client in OpenAI format @@ -174,12 +195,15 @@ async def completion( self, request: CreateCompletionRequest ) -> CreateCompletionResponse | AsyncIterator[str]: # Validate request and convert to Triton format - metadata = self.model_metadata.get(request.model) - self._validate_completion_request(request, metadata) + model_name, lora_name = self._get_model_and_lora_name(request.model) + metadata = self.model_metadata.get(model_name) + self._validate_completion_request(request, metadata, lora_name) # Convert to Triton request format and perform inference responses = metadata.model.async_infer( - metadata.request_converter(metadata.model, request.prompt, request) + metadata.request_converter( + metadata.model, request.prompt, request, lora_name + ) ) # Prepare and send responses back to client in OpenAI format @@ -187,7 +211,7 @@ async def completion( created = int(time.time()) if request.stream: return self._streaming_completion_iterator( - request_id, created, metadata.name, responses + request_id, created, request.model, responses ) # Response validation with decoupled models in mind @@ -208,7 +232,7 @@ async def completion( system_fingerprint=None, object=ObjectType.text_completion, created=created, - model=metadata.name, + model=request.model, ) # TODO: This behavior should be tested further @@ -234,6 +258,16 @@ def _determine_request_converter(self, backend: str): # an ensemble, a python or BLS model, a TRT-LLM backend model, etc. return _create_trtllm_inference_request + def _get_model_and_lora_name(self, request_model_name: str): + if self.lora_separator is None or len(self.lora_separator) == 0: + return request_model_name, None + + names = request_model_name.split(self.lora_separator) + if len(names) != 2: + return request_model_name, None + + return names[0], names[1] + def _get_tokenizer(self, tokenizer_name: str): tokenizer = None if tokenizer_name: @@ -254,11 +288,18 @@ def _get_model_metadata(self) -> Dict[str, TritonModelMetadata]: backend = "ensemble" print(f"Found model: {name=}, {backend=}") + lora_names = None + if self.backend == "vllm" or backend == "vllm": + lora_names = _get_vllm_lora_names( + self.server.options.model_repository, name, model.version + ) + metadata = TritonModelMetadata( name=name, backend=backend, model=model, tokenizer=self.tokenizer, + lora_names=lora_names, create_time=self.create_time, request_converter=self._determine_request_converter(backend), ) @@ -343,7 +384,10 @@ async def _streaming_chat_iterator( yield "data: [DONE]\n\n" def _validate_chat_request( - self, request: CreateChatCompletionRequest, metadata: TritonModelMetadata + self, + request: CreateChatCompletionRequest, + metadata: TritonModelMetadata, + lora_name: str | None, ): """ Validates a chat request to align with currently supported features. @@ -362,6 +406,13 @@ def _validate_chat_request( if not metadata.request_converter: raise Exception(f"Unknown request format for model: {request.model}") + if ( + metadata.lora_names is not None + and lora_name is not None + and lora_name not in metadata.lora_names + ): + raise Exception(f"Unknown LoRA: {lora_name}; for model: {request.model}") + # Reject unsupported features if requested if request.n and request.n > 1: raise Exception( @@ -396,7 +447,10 @@ async def _streaming_completion_iterator( yield "data: [DONE]\n\n" def _validate_completion_request( - self, request: CreateCompletionRequest, metadata: TritonModelMetadata + self, + request: CreateCompletionRequest, + metadata: TritonModelMetadata, + lora_name: str | None, ): """ Validates a completions request to align with currently supported features. @@ -411,6 +465,13 @@ def _validate_completion_request( if not metadata.request_converter: raise Exception(f"Unknown request format for model: {request.model}") + if ( + metadata.lora_names is not None + and lora_name is not None + and lora_name not in metadata.lora_names + ): + raise Exception(f"Unknown LoRA: {lora_name}; for model: {request.model}") + # Reject unsupported features if requested if request.suffix is not None: raise Exception("suffix is not currently supported") diff --git a/python/openai/openai_frontend/engine/utils/triton.py b/python/openai/openai_frontend/engine/utils/triton.py index 2ec8cce7d5..438890a05a 100644 --- a/python/openai/openai_frontend/engine/utils/triton.py +++ b/python/openai/openai_frontend/engine/utils/triton.py @@ -1,4 +1,4 @@ -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -25,6 +25,9 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import ctypes +import json +import os +import re from typing import Iterable, List import numpy as np @@ -33,7 +36,10 @@ def _create_vllm_inference_request( - model, prompt, request: CreateChatCompletionRequest | CreateCompletionRequest + model, + prompt, + request: CreateChatCompletionRequest | CreateCompletionRequest, + lora_name: str | None, ): inputs = {} # Exclude non-sampling parameters so they aren't passed to vLLM @@ -59,10 +65,13 @@ def _create_vllm_inference_request( # NOTE: The exclude_none is important, as internals may not support # values of NoneType at this time. - sampling_parameters = request.model_dump_json( + sampling_parameters = request.model_dump( exclude=excludes, exclude_none=True, ) + if lora_name is not None: + sampling_parameters["lora_name"] = lora_name + sampling_parameters = json.dumps(sampling_parameters) exclude_input_in_output = True echo = getattr(request, "echo", None) @@ -79,8 +88,14 @@ def _create_vllm_inference_request( def _create_trtllm_inference_request( - model, prompt, request: CreateChatCompletionRequest | CreateCompletionRequest + model, + prompt, + request: CreateChatCompletionRequest | CreateCompletionRequest, + lora_name: str | None, ): + if lora_name is not None: + raise Exception("LoRA selection is currently not supported for TRT-LLM backend") + inputs = {} inputs["text_input"] = [[prompt]] inputs["stream"] = np.bool_([[request.stream]]) @@ -173,3 +188,50 @@ def _validate_triton_responses_non_streaming( raise Exception("Unexpected internal error with incorrect response flags") if num_responses > 2: raise Exception(f"Unexpected number of responses: {num_responses}, expected 1.") + + +def _get_vllm_lora_names( + model_repository: str | list[str], model_name: str, model_version: int +) -> None | List[str]: + lora_names = [] + repo_paths = model_repository + if isinstance(repo_paths, str): + repo_paths = [repo_paths] + for repo_path in repo_paths: + model_path = os.path.join(repo_path, model_name) + if not os.path.isdir(model_path): + # Cloud path? + return None + if model_version <= 0: + for version_path in os.listdir(model_path): + version = os.path.basename(version_path) + if re.fullmatch(r"^[0-9]+$", version) is None: + continue + model_version = max(model_version, int(version)) + if model_version <= 0: + # Model directory is malformed? + return None + version_path = os.path.join(model_path, str(model_version)) + is_lora_enabled = False + model_file_path = os.path.join(version_path, "model.json") + try: + with open(model_file_path, "r") as f: + config = json.load(f) + if "enable_lora" in config: + # The value could be a string or a bool. + is_lora_enabled = str(config["enable_lora"]).lower() == "true" + except Exception: + # Model directory or model.json is malformed? + return None + if is_lora_enabled != True: + continue + lora_config_path = os.path.join(version_path, "multi_lora.json") + try: + with open(lora_config_path, "r") as f: + lora_config = json.load(f) + for lora_name in lora_config.keys(): + lora_names.append(lora_name) + except Exception: + # LoRA is enabled but its list is not provided or malformed? + return None + return lora_names diff --git a/python/openai/openai_frontend/main.py b/python/openai/openai_frontend/main.py index 5a4e2368a0..d8899875c4 100755 --- a/python/openai/openai_frontend/main.py +++ b/python/openai/openai_frontend/main.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -108,6 +108,12 @@ def parse_args(): choices=["vllm", "tensorrtllm"], help="Manual override of Triton backend request format (inputs/output names) to use for inference", ) + triton_group.add_argument( + "--lora-separator", + type=str, + default=None, + help="LoRA name selection may be appended to the model name following this separator if the separator is provided", + ) triton_group.add_argument( "--tritonserver-log-verbose-level", type=int, @@ -171,7 +177,10 @@ def main(): # Wrap Triton Inference Server in an interface-conforming "LLMEngine" engine: TritonLLMEngine = TritonLLMEngine( - server=server, tokenizer=args.tokenizer, backend=args.backend + server=server, + tokenizer=args.tokenizer, + backend=args.backend, + lora_separator=args.lora_separator, ) # Attach TritonLLMEngine as the backbone for inference and model management diff --git a/python/openai/tests/test_lora.py b/python/openai/tests/test_lora.py new file mode 100644 index 0000000000..2c89789bbc --- /dev/null +++ b/python/openai/tests/test_lora.py @@ -0,0 +1,353 @@ +# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import json +import os +import shutil +import unittest + +from huggingface_hub import snapshot_download +from openai import BadRequestError, NotFoundError + +from .utils import OpenAIServer + + +def is_vllm_installed(): + try: + import vllm as _ + + return True + except ImportError: + return False + + +class LoRATest(unittest.TestCase): + _model_name = "gemma-2b" + # TODO: Find a LoRA model that has its own tokenizer. + _tokenizer = "meta-llama/Meta-Llama-3.1-8B-Instruct" + _lora_separator = "_lora_" + _prompt = "When was the wheel invented?" + # more prompts that may yield different outputs: + # - "Why can camels survive for long without water?" + # - "What is LAPR?" + # - "What is the difference between pets and cattle?" + _temperature = 0 + _top_p = 1 + + def setUp(self): + self._completions_outputs = {} + self._chat_completion_outputs = {} + + def _create_model_repository_with_lora(self): + shutil.rmtree("models", ignore_errors=True) + os.makedirs(f"models/{self._model_name}/1", exist_ok=True) + with open(f"models/{self._model_name}/config.pbtxt", "w") as f: + f.write('backend: "vllm"') + with open(f"models/{self._model_name}/1/model.json", "w") as f: + f.write( + json.dumps( + { + "model": "unsloth/gemma-2b", + "enable_lora": True, + "max_lora_rank": 32, + } + ) + ) + with open(f"models/{self._model_name}/1/multi_lora.json", "w") as f: + f.write( + json.dumps( + { + "doll": f"models/{self._model_name}/1/GemmaDoll", + "sheep": f"models/{self._model_name}/1/GemmaSheep", + } + ) + ) + snapshot_download( + repo_id="swathijn/GemmaDoll-2b-dolly-LORA-Tune", + local_dir=f"models/{self._model_name}/1/GemmaDoll", + ) + snapshot_download( + repo_id="eduardo-alvarez/GemmaSheep-2B-LORA-TUNED", + local_dir=f"models/{self._model_name}/1/GemmaSheep", + ) + + def _create_model_repository_without_lora(self): + shutil.rmtree("models", ignore_errors=True) + os.makedirs(f"models/{self._model_name}/1", exist_ok=True) + with open(f"models/{self._model_name}/config.pbtxt", "w") as f: + f.write('backend: "vllm"') + with open(f"models/{self._model_name}/1/model.json", "w") as f: + f.write(json.dumps({"model": "unsloth/gemma-2b"})) + + def _create_model_repository_mock_llm(self): + shutil.rmtree("models", ignore_errors=True) + os.makedirs(f"models/{self._model_name}/1", exist_ok=True) + with open(f"models/{self._model_name}/config.pbtxt", "w") as f: + f.write( + """ + backend: "python" + max_batch_size: 0 + model_transaction_policy { decoupled: True } + input [ + { + name: "text_input" + data_type: TYPE_STRING + dims: [ 1 ] + }, + { + name: "stream" + data_type: TYPE_BOOL + dims: [ 1 ] + }, + { + name: "sampling_parameters" + data_type: TYPE_STRING + dims: [ 1 ] + }, + { + name: "exclude_input_in_output" + data_type: TYPE_BOOL + dims: [ 1 ] + } + ] + output [ + { + name: "text_output" + data_type: TYPE_STRING + dims: [ -1 ] + } + ] + """ + ) + shutil.copy( + "tests/test_models/mock_llm/1/model.py", f"models/{self._model_name}/1" + ) + + def _get_model_name(self, lora_name): + model_name = self._model_name + if lora_name != "": + model_name += f"{self._lora_separator}{lora_name}" + return model_name + + def _test_list_models(self, client, expected_lora_names): + expected_model_names = [] + for lora_name in expected_lora_names: + expected_model_names.append(self._get_model_name(lora_name)) + models = client.models.list() + for model in models: + self.assertIn(model.id, expected_model_names) + expected_model_names.remove(model.id) + self.assertEqual(len(expected_model_names), 0) + + def _test_retrieve_model(self, client, lora_name): + model_name = self._get_model_name(lora_name) + model = client.models.retrieve(model_name) + self.assertEqual(model.id, model_name) + + def _test_completions(self, client, lora_name): + model_name = self._get_model_name(lora_name) + completion = client.completions.create( + model=model_name, + prompt=self._prompt, + temperature=self._temperature, + top_p=self._top_p, + ) + self.assertEqual(completion.model, model_name) + output = completion.choices[0].text + for other_output in self._completions_outputs.values(): + self.assertNotEqual( + output, + other_output, + msg=f"other completions outputs: {self._completions_outputs}", + ) + self._completions_outputs[lora_name] = output + + def _test_chat_completion(self, client, lora_name): + model_name = self._get_model_name(lora_name) + messages = [{"role": "user", "content": self._prompt}] + chat_completion = client.chat.completions.create( + model=model_name, + messages=messages, + temperature=self._temperature, + top_p=self._top_p, + ) + self.assertEqual(chat_completion.model, model_name) + output = chat_completion.choices[0].message.content + for other_output in self._chat_completion_outputs.values(): + self.assertNotEqual( + output, + other_output, + msg=f"other chat outputs: {self._chat_completion_outputs}", + ) + self._chat_completion_outputs[lora_name] = output + + @unittest.skipUnless(is_vllm_installed(), "vLLM not installed") + def test_lora_separator_not_set(self): + self._create_model_repository_with_lora() + with OpenAIServer( + cli_args=[ + "--model-repository", + "models", + "--tokenizer", + self._tokenizer, + ], + env_dict={"CUDA_VISIBLE_DEVICES": "0"}, + ) as server: + client = server.get_client() + # Test listing/retrieving models + self._test_list_models(client, [""]) + self._test_retrieve_model(client, "") + with self.assertRaises(NotFoundError) as e: + self._test_retrieve_model(client, "doll") + expected_error = f"Error code: 404 - {{'detail': 'Unknown model: {self._model_name}{self._lora_separator}doll'}}" + self.assertEqual(str(e.exception), expected_error) + with self.assertRaises(NotFoundError) as e: + self._test_retrieve_model(client, "sheep") + expected_error = f"Error code: 404 - {{'detail': 'Unknown model: {self._model_name}{self._lora_separator}sheep'}}" + self.assertEqual(str(e.exception), expected_error) + # Test selecting LoRAs + self._test_completions(client, "") + self._test_chat_completion(client, "") + with self.assertRaises(BadRequestError) as e: + self._test_completions(client, "doll") + expected_error = f"Error code: 400 - {{'detail': 'Unknown model: {self._model_name}{self._lora_separator}doll'}}" + self.assertEqual(str(e.exception), expected_error) + with self.assertRaises(BadRequestError) as e: + self._test_chat_completion(client, "sheep") + expected_error = f"Error code: 400 - {{'detail': 'Unknown model: {self._model_name}{self._lora_separator}sheep'}}" + self.assertEqual(str(e.exception), expected_error) + + @unittest.skipUnless(is_vllm_installed(), "vLLM not installed") + def test_lora_separator_set(self): + self._create_model_repository_with_lora() + with OpenAIServer( + cli_args=[ + "--model-repository", + "models", + "--tokenizer", + self._tokenizer, + "--lora-separator", + self._lora_separator, + ], + env_dict={"CUDA_VISIBLE_DEVICES": "0"}, + ) as server: + client = server.get_client() + # Test listing/retrieving models + self._test_list_models(client, ["", "doll", "sheep"]) + self._test_retrieve_model(client, "") + self._test_retrieve_model(client, "doll") + self._test_retrieve_model(client, "sheep") + # Test retrieving LoRAs unknown to the backend + with self.assertRaises(NotFoundError) as e: + self._test_retrieve_model(client, "unknown") + expected_error = f"Error code: 404 - {{'detail': 'Unknown model: {self._model_name}{self._lora_separator}unknown'}}" + self.assertEqual(str(e.exception), expected_error) + # Test selecting LoRAs + self._test_completions(client, "") + self._test_completions(client, "doll") + self._test_completions(client, "sheep") + self._test_chat_completion(client, "") + self._test_chat_completion(client, "doll") + self._test_chat_completion(client, "sheep") + # Test selecting LoRAs unknown to the backend + expected_error = f"Error code: 400 - {{'detail': 'Unknown LoRA: unknown; for model: {self._model_name}{self._lora_separator}unknown'}}" + with self.assertRaises(BadRequestError) as e: + self._test_completions(client, "unknown") + self.assertEqual(str(e.exception), expected_error) + with self.assertRaises(BadRequestError) as e: + self._test_chat_completion(client, "unknown") + self.assertEqual(str(e.exception), expected_error) + + @unittest.skipUnless(is_vllm_installed(), "vLLM not installed") + def test_lora_separator_set_for_lora_off_model(self): + self._create_model_repository_without_lora() + with OpenAIServer( + cli_args=[ + "--model-repository", + "models", + "--tokenizer", + self._tokenizer, + "--lora-separator", + self._lora_separator, + ], + env_dict={"CUDA_VISIBLE_DEVICES": "0"}, + ) as server: + client = server.get_client() + # Test listing/retrieving models + self._test_list_models(client, [""]) + self._test_retrieve_model(client, "") + # Test retrieving models with LoRAs + with self.assertRaises(NotFoundError) as e: + self._test_retrieve_model(client, "doll") + expected_error = f"Error code: 404 - {{'detail': 'Unknown model: {self._model_name}{self._lora_separator}doll'}}" + self.assertEqual(str(e.exception), expected_error) + # Test inference + self._test_completions(client, "") + self._test_chat_completion(client, "") + # Test selecting LoRAs + expected_error = f"Error code: 400 - {{'detail': 'Unknown LoRA: sheep; for model: {self._model_name}{self._lora_separator}sheep'}}" + with self.assertRaises(BadRequestError) as e: + self._test_completions(client, "sheep") + self.assertEqual(str(e.exception), expected_error) + with self.assertRaises(BadRequestError) as e: + self._test_chat_completion(client, "sheep") + self.assertEqual(str(e.exception), expected_error) + + @unittest.skipUnless(is_vllm_installed(), "vLLM not installed") + def test_lora_separator_set_for_non_vllm_formatted_models(self): + self._create_model_repository_mock_llm() + with OpenAIServer( + cli_args=[ + "--model-repository", + "models", + "--tokenizer", + self._tokenizer, + "--backend", + "vllm", + "--lora-separator", + self._lora_separator, + ], + env_dict={"CUDA_VISIBLE_DEVICES": "0"}, + ) as server: + client = server.get_client() + # Test listing/retrieving models + self._test_list_models(client, [""]) + self._test_retrieve_model(client, "") + # Test retrieving models with LoRAs + with self.assertRaises(NotFoundError) as e: + self._test_retrieve_model(client, "sheep") + expected_error = f"Error code: 404 - {{'detail': 'Unknown model: {self._model_name}{self._lora_separator}sheep'}}" + self.assertEqual(str(e.exception), expected_error) + # Test selecting LoRAs + # Expectation: + # If the frontend cannot determine which LoRA(s) are available, then any + # request with a well-formed LoRA model name will be inferenced. + self._test_completions(client, "doll") + self._test_chat_completion(client, "doll") + + +if __name__ == "__main__": + unittest.main() diff --git a/python/openai/tests/utils.py b/python/openai/tests/utils.py index fdffcc5ea9..3ed46f70b4 100644 --- a/python/openai/tests/utils.py +++ b/python/openai/tests/utils.py @@ -1,4 +1,4 @@ -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -63,7 +63,7 @@ def setup_fastapi_app(tokenizer: str, server: tritonserver.Server, backend: str) # Heavily inspired by vLLM's test infrastructure class OpenAIServer: API_KEY = "EMPTY" # Triton's OpenAI server does not need API key - START_TIMEOUT = 120 # wait for server to start for up to 120 seconds + START_TIMEOUT = 180 # wait for server to start in seconds def __init__( self, From 292dfd95c0a4d3d316b29a00b30ab570a8f5e075 Mon Sep 17 00:00:00 2001 From: richardhuo-nv Date: Thu, 17 Apr 2025 16:54:12 -0700 Subject: [PATCH 017/230] feat: add the tool calling to the openai frontend (#8134) Add the tool calling parsers implementation to openai frontend, the available parsers are llama3 and mistral. Most of the implementation is from the vllm. A user could use the --tool-call-parser arguments to specify the tool parser. Add the --chat-template {chat template file path} argument to allow the user use the customized template to better tune the prompt for tool calling. Add the guided decoding backend integration with the tool calling to enable the named tool calling and required tool calling functionalities. Please check more detail in the change of README.md All changes in python/openai/openai_frontend/engine/utils/tool_call_parsers are from the vLLM with some minor compatibility changes. --- .gitignore | 2 + python/openai/README.md | 250 ++++++++ .../openai_frontend/engine/triton_engine.py | 378 ++++++++++- .../openai_frontend/engine/utils/chat.py | 165 +++++ .../openai_frontend/engine/utils/tokenizer.py | 4 +- .../utils/tool_call_parsers/__init__.py | 40 ++ .../llama_tool_call_parser.py | 289 +++++++++ .../mistral_tool_call_parser.py | 318 ++++++++++ .../tool_call_parsers/tool_call_parser.py | 133 ++++ .../engine/utils/tool_call_parsers/utils.py | 129 ++++ .../openai_frontend/engine/utils/triton.py | 50 +- python/openai/openai_frontend/main.py | 18 + .../openai/openai_frontend/schemas/openai.py | 14 +- python/openai/requirements.txt | 1 + python/openai/tests/conftest.py | 49 +- python/openai/tests/test_chat_completions.py | 4 +- python/openai/tests/test_completions.py | 3 +- python/openai/tests/test_openai_client.py | 9 +- python/openai/tests/test_tool_calling.py | 592 ++++++++++++++++++ python/openai/tests/utils.py | 2 +- .../mistral-nemo-instruct-2407/1/model.json | 1 + .../mistral-nemo-instruct-2407/config.pbtxt | 28 + qa/L0_openai/generate_engine.py | 56 ++ qa/L0_openai/test.sh | 47 +- 24 files changed, 2495 insertions(+), 87 deletions(-) create mode 100644 python/openai/openai_frontend/engine/utils/chat.py create mode 100644 python/openai/openai_frontend/engine/utils/tool_call_parsers/__init__.py create mode 100644 python/openai/openai_frontend/engine/utils/tool_call_parsers/llama_tool_call_parser.py create mode 100644 python/openai/openai_frontend/engine/utils/tool_call_parsers/mistral_tool_call_parser.py create mode 100644 python/openai/openai_frontend/engine/utils/tool_call_parsers/tool_call_parser.py create mode 100644 python/openai/openai_frontend/engine/utils/tool_call_parsers/utils.py create mode 100644 python/openai/tests/test_tool_calling.py create mode 100644 python/openai/tests/vllm_mistral_models/mistral-nemo-instruct-2407/1/model.json create mode 100644 python/openai/tests/vllm_mistral_models/mistral-nemo-instruct-2407/config.pbtxt create mode 100644 qa/L0_openai/generate_engine.py diff --git a/.gitignore b/.gitignore index bce94b6830..1e0b66480b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ test_results.txt artifacts cprofile *.prof +.venv +**/.venv # Test exclusions qa/L0_openai/openai diff --git a/python/openai/README.md b/python/openai/README.md index ff642555b5..4e6f05b44a 100644 --- a/python/openai/README.md +++ b/python/openai/README.md @@ -405,3 +405,253 @@ For more information on the `tritonfrontend` python bindings, see the docs - Set the following environment variable: `export TRTLLM_ORCHESTRATOR=1` - [ ] TensorRT-LLM ([Leader Mode](https://github.com/triton-inference-server/tensorrtllm_backend/blob/main/README.md#leader-mode)) - Not currently supported + +## Tool Calling + +The OpenAI frontend supports `tools` and `tool_choice` in the `v1/chat/completions` API. Please refer to the OpenAI API reference for more details about these parameters: + [tools](https://platform.openai.com/docs/api-reference/chat/create#chat-create-tools), + [tool_choice](https://platform.openai.com/docs/api-reference/chat/create#chat-create-tool_choice) + +To enable the tool-calling feature, add the `--tool-call-parser {parser_name}` flag when starting the server. The two available parsers are `llama3` and `mistral`. +The `llama3` parser supports tool-calling features for LLaMA 3.1, 3.2, and 3.3 models, while the `mistral` parser supports tool-calling features for the Mistral Instruct model. + +Example for launching the OpenAI frontend with a tool call parser: +``` +python3 openai_frontend/main.py \ + --model-repository tests/vllm_models \ + --tokenizer meta-llama/Meta-Llama-3.1-8B-Instruct \ + --tool-call-parser llama3 +``` + +Example for making a tool calling request: + +```python +import json +from openai import OpenAI + + +def get_current_weather(city: str, state: str, unit: "str"): + return ( + "The weather in Dallas, Texas is 85 degrees fahrenheit. It is " + "partly cloudly, with highs in the 90's." + ) + +available_tools = {"get_current_weather": get_current_weather} + +openai_api_key = "EMPTY" +openai_api_base = "http://localhost:9000/v1" + +client = OpenAI( + api_key=openai_api_key, + base_url=openai_api_base, +) + +model = "llama-3.1-8b-instruct" # change this to the model in the repository + +tools = [ + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "city": { + "type": "string", + "description": "The city to find the weather for, e.g. 'San Francisco'", + }, + "state": { + "type": "string", + "description": "the two-letter abbreviation for the state that the city is" + " in, e.g. 'CA' which would mean 'California'", + }, + "unit": { + "type": "string", + "description": "The unit to fetch the temperature in", + "enum": ["celsius", "fahrenheit"], + }, + }, + "required": ["city", "state", "unit"], + }, + }, + } +] + +messages = [ + { + "role": "system", + "content": "You're a helpful assistant! Answer the users question best you can.", + }, + {"role": "user", "content": "What is the weather in Dallas, Texas in Fahrenheit?"}, +] + +tool_calls = client.chat.completions.create( + messages=messages, model=model, tools=tools, max_tokens=128 +) +function_name = tool_calls.choices[0].message.tool_calls[0].function.name +function_arguments = tool_calls.choices[0].message.tool_calls[0].function.arguments + +print(f"function name: " f"{function_name}") +print(f"function arguments: {function_arguments}") +print(f"tool calling result: {available_tools[function_name](**json.loads(function_arguments))}") +``` + +Example output: +``` +function name: get_current_weather +function arguments: {"city": "Dallas", "state": "TX", "unit": "fahrenheit"} +tool calling result: The weather in Dallas, Texas is 85 degrees fahrenheit. It is partly cloudly, with highs in the 90's. +``` + + +> [!WARNING] +> When using LangChain to call the `v1/chat/completions` endpoint, you might encounter an exception related to `max_completion_tokens` if you have specified `max_tokens` in the request. +> +> Example: `openai.BadRequestError: Error code: 400 - {'object': 'error', 'message': "[{'type': 'extra_forbidden', 'loc': ('body', 'max_completion_tokens'), 'msg': 'Extra inputs are not permitted', 'input': 800}]", 'type': 'BadRequestError', 'param': None, 'code': 400}` +> +> This issue is due to an incompatibility between Triton's OpenAI API frontend and the latest OpenAI API. We are actively working to address this gap. A workaround is adding the `max_tokens` into the `model_kwargs` of the LangChain OpenAI request. +> +> Example: +```python +from langchain.llms import OpenAI + +llm = OpenAI( + model_name="llama-3.1-8b-instruct", + temperature=0.0, + model_kwargs={ + "max_tokens": 4096 + } +) + +response = llm("Write a short poem about a sunset.") +print(response) + +``` + +#### Named Tool Calling + +The OpenAI frontend supports named function calling, utilizing guided decoding in the vLLM and TensorRT-LLM backends. Users can specify one of the tools in `tool_choice` to force the model to select a specific tool for function calling. + +> [!NOTE] +> The latest release of TensorRT-LLM (v0.18.0) does not yet support guided decoding. To enable this feature, use a build from the main branch of TensorRT-LLM. +> For instructions on enabling guided decoding in the TensorRT-LLM backend, please refer to [this guide](https://github.com/triton-inference-server/tensorrtllm_backend/blob/main/docs/guided_decoding.md) + +Example for making a named tool calling request: + +```python +import json +from openai import OpenAI + + +def get_current_weather(city: str, state: str, unit: "str"): + return ( + "The weather in Dallas, Texas is 85 degrees fahrenheit. It is " + "partly cloudly, with highs in the 90's." + ) + +def get_n_day_weather_forecast(city: str, state: str, unit: str, num_days: int): + return ( + f"The weather in Dallas, Texas is 85 degrees fahrenheit in next {num_days} days." + ) + +available_tools = {"get_current_weather": get_current_weather, + "get_n_day_weather_forecast": get_n_day_weather_forecast} + +openai_api_key = "EMPTY" +openai_api_base = "http://localhost:9000/v1" +client = OpenAI( + api_key=openai_api_key, + base_url=openai_api_base, +) +model = "llama-3.1-8b-instruct" # change this to the model in the repository +tools = [ + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "city": { + "type": "string", + "description": "The city to find the weather for, e.g. 'San Francisco'", + }, + "state": { + "type": "string", + "description": "the two-letter abbreviation for the state that the city is" + " in, e.g. 'CA' which would mean 'California'", + }, + "unit": { + "type": "string", + "description": "The unit to fetch the temperature in", + "enum": ["celsius", "fahrenheit"], + }, + }, + "required": ["city", "state", "unit"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "get_n_day_weather_forecast", + "description": "Get an N-day weather forecast", + "parameters": { + "type": "object", + "properties": { + "city": { + "type": "string", + "description": "The city to find the weather for, " + "e.g. 'San Francisco'", + }, + "state": { + "type": "string", + "description": "must the two-letter abbreviation for the state " + "that the city is in, e.g. 'CA' which would " + "mean 'California'", + }, + "unit": { + "type": "string", + "description": "The unit to fetch the temperature in", + "enum": ["celsius", "fahrenheit"], + }, + "num_days": { + "type": "integer", + "description": "The number of days to forecast", + }, + }, + "required": ["city", "state", "unit", "num_days"], + }, + }, + } +] + +tool_choice = {"function": {"name": "get_n_day_weather_forecast"}, "type": "function"} + +messages = [ + { + "role": "system", + "content": "You're a helpful assistant! Answer the users question best you can.", + }, + {"role": "user", "content": "What is the weather in Dallas, Texas in Fahrenheit?"}, +] + +tool_calls = client.chat.completions.create( + messages=messages, model=model, tools=tools, tool_choice=tool_choice, max_tokens=128 +) +function_name = tool_calls.choices[0].message.tool_calls[0].function.name +function_arguments = tool_calls.choices[0].message.tool_calls[0].function.arguments + +print(f"function name: "{function_name}") +print(f"function arguments: {function_arguments}") +print(f"tool calling result: {available_tools[function_name](**json.loads(function_arguments))}") +``` + +Example output: +``` +function name: get_n_day_weather_forecast +function arguments: {"city": "Dallas", "state": "TX", "unit": "fahrenheit", num_days: 1} +tool calling result: The weather in Dallas, Texas is 85 degrees fahrenheit in next 1 days. +``` \ No newline at end of file diff --git a/python/openai/openai_frontend/engine/triton_engine.py b/python/openai/openai_frontend/engine/triton_engine.py index de6c1ed2cf..32357cfdd4 100644 --- a/python/openai/openai_frontend/engine/triton_engine.py +++ b/python/openai/openai_frontend/engine/triton_engine.py @@ -27,14 +27,26 @@ from __future__ import annotations +import json import time import uuid from dataclasses import dataclass -from typing import Any, AsyncIterable, AsyncIterator, Callable, Dict, List, Optional +from typing import ( + Any, + AsyncIterable, + AsyncIterator, + Callable, + Dict, + List, + Optional, + Tuple, +) import tritonserver from engine.engine import LLMEngine +from engine.utils.chat import load_chat_template, parse_chat_messages from engine.utils.tokenizer import get_tokenizer +from engine.utils.tool_call_parsers import ToolCallParser, ToolParserManager from engine.utils.triton import ( _create_trtllm_inference_request, _create_vllm_inference_request, @@ -45,9 +57,13 @@ from schemas.openai import ( ChatCompletionChoice, ChatCompletionFinishReason, + ChatCompletionMessageToolCall, + ChatCompletionMessageToolCallChunk, + ChatCompletionNamedToolChoice, ChatCompletionResponseMessage, ChatCompletionStreamingResponseChoice, ChatCompletionStreamResponseDelta, + ChatCompletionToolChoiceOption1, Choice, CreateChatCompletionRequest, CreateChatCompletionResponse, @@ -55,6 +71,8 @@ CreateCompletionRequest, CreateCompletionResponse, FinishReason, + Function1, + Function2, Model, ObjectType, ) @@ -86,6 +104,8 @@ def __init__( tokenizer: str, backend: Optional[str] = None, lora_separator: Optional[str] = None, + tool_call_parser: Optional[str] = None, + chat_template: Optional[str] = None, ): # Assume an already configured and started server self.server = server @@ -98,6 +118,12 @@ def __init__( # now, and won't account for dynamically loading/unloading models. self.create_time = int(time.time()) self.model_metadata = self._get_model_metadata() + self.tool_call_parser = ( + ToolParserManager.get_tool_parser_cls(tool_call_parser) + if tool_call_parser + else None + ) + self.chat_template = load_chat_template(chat_template) def ready(self) -> bool: return self.server.ready() @@ -138,15 +164,22 @@ async def chat( metadata = self.model_metadata.get(model_name) self._validate_chat_request(request, metadata, lora_name) - conversation = [ - message.model_dump(exclude_none=True) for message in request.messages - ] + conversation = parse_chat_messages(request.messages) + add_generation_prompt = True + tool_dicts = ( + None + if request.tools is None + else [tool.model_dump() for tool in request.tools] + ) + prompt = metadata.tokenizer.apply_chat_template( conversation=conversation, tokenize=False, add_generation_prompt=add_generation_prompt, + tools=tool_dicts, + chat_template=self.chat_template, ) # Convert to Triton request format and perform inference @@ -162,9 +195,19 @@ async def chat( conversation, add_generation_prompt, default_role ) + tool_call_parser = ( + self.tool_call_parser(metadata.tokenizer) if self.tool_call_parser else None + ) + if request.stream: return self._streaming_chat_iterator( - request_id, created, request.model, role, responses + request_id, + metadata.backend, + created, + request, + role, + tool_call_parser, + responses, ) # Response validation with decoupled models in mind @@ -173,16 +216,23 @@ async def chat( response = responses[0] text = _get_output(response) + response_message, finish_reason = self._get_chat_completion_response_message( + request=request, + request_id=request_id, + tool_call_parser=tool_call_parser, + text=text, + role=role, + backend=metadata.backend, + ) + return CreateChatCompletionResponse( id=request_id, choices=[ ChatCompletionChoice( index=0, - message=ChatCompletionResponseMessage( - content=text, role=role, function_call=None - ), + message=response_message, logprobs=None, - finish_reason=ChatCompletionFinishReason.stop, + finish_reason=finish_reason, ) ], created=created, @@ -191,6 +241,56 @@ async def chat( object=ObjectType.chat_completion, ) + def _get_chat_completion_response_message( + self, + request: CreateChatCompletionRequest, + request_id: str, + tool_call_parser: ToolCallParser, + text: str, + role: str, + backend: str, + ) -> Tuple[ChatCompletionResponseMessage, ChatCompletionFinishReason]: + response_message: ChatCompletionResponseMessage + auto_tools_called = False + tool_function_name = self._get_named_function_name(request=request) + if tool_function_name: + response_message = ChatCompletionResponseMessage( + content="", + role=role, + tool_calls=[ + ChatCompletionMessageToolCall( + id=request_id, + type="function", + function=Function1(name=tool_function_name, arguments=text), + ) + ], + ) + elif ( + tool_call_parser + and request.tools + and ( + request.tool_choice is None + or request.tool_choice.root == ChatCompletionToolChoiceOption1.auto + ) + ): + response_message = tool_call_parser.parse_tool_calls(text, role, backend) + auto_tools_called = ( + response_message.tool_calls is not None + and len(response_message.tool_calls.root) > 0 + ) + else: + response_message = ChatCompletionResponseMessage( + content=text, role=role, tool_calls=None + ) + + finish_reason = ( + ChatCompletionFinishReason.tool_calls + if auto_tools_called + else ChatCompletionFinishReason.stop + ) + + return response_message, finish_reason + async def completion( self, request: CreateCompletionRequest ) -> CreateCompletionResponse | AsyncIterator[str]: @@ -340,49 +440,179 @@ def _get_first_streaming_chat_response( ) return chunk - def _get_nth_streaming_chat_response( - self, - request_id: str, - created: int, - model: str, - response: tritonserver.InferenceResponse, - ) -> CreateChatCompletionStreamResponse: - text = _get_output(response) - choice = ChatCompletionStreamingResponseChoice( - index=0, - delta=ChatCompletionStreamResponseDelta( - role=None, content=text, function_call=None - ), - logprobs=None, - finish_reason=ChatCompletionFinishReason.stop if response.final else None, - ) - - chunk = self._get_streaming_chat_response_chunk( - choice, request_id, created, model - ) - return chunk - async def _streaming_chat_iterator( self, request_id: str, + backend: str, created: int, - model: str, + request: CreateChatCompletionRequest, role: str, + tool_call_parser: ToolCallParser, responses: AsyncIterable, ) -> AsyncIterator[str]: + model = request.model + + tool_function_name = self._get_named_function_name(request=request) + + # Determine whether tools are in use with "auto" tool choice + tool_choice_auto = ( + tool_call_parser + and not tool_function_name + and self._should_stream_with_auto_tool_parsing(request) + ) + + previous_text = "" + chunk = self._get_first_streaming_chat_response( request_id, created, model, role ) yield f"data: {chunk.model_dump_json(exclude_unset=True)}\n\n" async for response in responses: - chunk = self._get_nth_streaming_chat_response( - request_id, created, model, response + delta_text = _get_output(response) + + ( + response_delta, + finish_reason, + current_text, + ) = self._get_streaming_response_delta( + previous_text=previous_text, + delta_text=delta_text, + tool_function_name=tool_function_name, + tool_choice_auto=tool_choice_auto, + tool_call_parser=tool_call_parser, + backend=backend, + is_final_response=response.final, + ) + previous_text = current_text + + # if the response delta is None (e.g. because it was a + # "control token" for tool calls or the parser otherwise + # wasn't ready to send a token, then + # get the next token without streaming a chunk + if response_delta is None and finish_reason is None: + continue + + if finish_reason and response_delta is None: + response_delta = ChatCompletionStreamResponseDelta(content="") + + choice = ChatCompletionStreamingResponseChoice( + index=0, + delta=response_delta, + logprobs=None, + finish_reason=finish_reason, + ) + + chunk = self._get_streaming_chat_response_chunk( + choice, request_id, created, model ) yield f"data: {chunk.model_dump_json(exclude_unset=True)}\n\n" yield "data: [DONE]\n\n" + def _get_streaming_response_delta( + self, + previous_text: str, + delta_text: str, + tool_function_name: Optional[str], + tool_choice_auto: bool, + tool_call_parser: ToolCallParser, + backend: str, + is_final_response: bool, + ) -> Tuple[ + Optional[ChatCompletionStreamResponseDelta], + Optional[ChatCompletionFinishReason], + str, + ]: + response_delta: Optional[ChatCompletionStreamResponseDelta] + current_text = "" + if tool_function_name: + response_delta = ChatCompletionStreamResponseDelta( + tool_calls=[ + ChatCompletionMessageToolCallChunk( + index=0, + function=Function2( + name=tool_function_name, arguments=delta_text + ), + ) + ] + ) + elif tool_choice_auto: + current_text = previous_text + delta_text + response_delta = tool_call_parser.parse_tool_calls_streaming( + current_text=current_text, delta_text=delta_text, backend=backend + ) + else: + response_delta = ChatCompletionStreamResponseDelta( + role=None, content=delta_text, function_call=None + ) + + if is_final_response: + auto_tools_called = False + if tool_call_parser: + auto_tools_called = len(tool_call_parser.prev_tool_call_arr) > 0 + index = ( + len(tool_call_parser.prev_tool_call_arr) - 1 + if auto_tools_called + else 0 + ) + else: + index = 0 + + # check to make sure we haven't "forgotten" to stream + # any tokens that were generated but previously + # matched by partial json parsing, such as '}'. + # only happens if we are NOT using guided decoding + if ( + self._should_check_for_unstreamed_tool_arg_tokens( + response_delta=response_delta, + auto_tools_called=auto_tools_called, + ) + and tool_call_parser + ): + latest_delta_len = 0 + if ( + isinstance(response_delta.tool_calls[0].function, Function2) + ) and isinstance(response_delta.tool_calls[0].function.arguments, str): + latest_delta_len = len( + response_delta.tool_calls[0].function.arguments + ) + # get the expected call based on partial JSON + # parsing which "autocompletes" the JSON + expected_call = json.dumps( + tool_call_parser.prev_tool_call_arr[index].get("arguments", {}), + ensure_ascii=False, + ) + # get what we've streamed so far for arguments + # for the current tool + actual_call = tool_call_parser.streamed_args_for_tool[index] + if latest_delta_len > 0: + actual_call = actual_call[:-latest_delta_len] + + # check to see if there's anything left to stream + remaining_call = expected_call.replace(actual_call, "", 1) + + response_delta = ChatCompletionStreamResponseDelta( + tool_calls=[ + ChatCompletionMessageToolCallChunk( + index=index, + function=Function2(arguments=remaining_call).model_dump( + exclude_none=True + ), + ) + ] + ) + + finish_reason = ( + ChatCompletionFinishReason.tool_calls + if auto_tools_called + else ChatCompletionFinishReason.stop + ) + else: + finish_reason = None + + return response_delta, finish_reason, current_text + def _validate_chat_request( self, request: CreateChatCompletionRequest, @@ -422,6 +652,43 @@ def _validate_chat_request( if request.logit_bias is not None or request.logprobs: raise Exception("logit bias and log probs not currently supported") + self._verify_chat_tool_call_settings(request=request) + + def _verify_chat_tool_call_settings(self, request: CreateChatCompletionRequest): + if ( + request.tool_choice + and request.tool_choice.root == ChatCompletionToolChoiceOption1.required + and not request.tools + ): + raise Exception( + '"required" tool choice requires CreateChatCompletionRequest.tools to be provided' + ) + + if ( + request.tool_choice + and isinstance(request.tool_choice.root, ChatCompletionNamedToolChoice) + and not request.tools + ): + raise Exception( + "Named tool choice requires CreateChatCompletionRequest.tools to be provided" + ) + + if ( + request.tool_choice + and request.tool_choice.root == ChatCompletionToolChoiceOption1.auto + and self.tool_call_parser is None + ): + raise Exception('"auto" tool choice requires --tool-call-parser to be set') + + if ( + request.tool_choice is None + and request.tools + and self.tool_call_parser is None + ): + raise Exception( + "having tools in the request requires --tool-call-parser to be set" + ) + async def _streaming_completion_iterator( self, request_id: str, created: int, model: str, responses: AsyncIterable ) -> AsyncIterator[str]: @@ -495,3 +762,46 @@ def _validate_completion_request( if request.logit_bias is not None or request.logprobs is not None: raise Exception("logit bias and log probs not supported") + + def _should_stream_with_auto_tool_parsing( + self, request: CreateChatCompletionRequest + ): + has_tools = request.tools and self.tool_call_parser + auto_tool = ( + request.tool_choice is None + or request.tool_choice.root == ChatCompletionToolChoiceOption1.auto + ) + return has_tools and auto_tool + + def _should_check_for_unstreamed_tool_arg_tokens( + self, response_delta: ChatCompletionStreamResponseDelta, auto_tools_called + ): + return bool( + auto_tools_called + and self.tool_call_parser + and response_delta + and response_delta.tool_calls + and response_delta.tool_calls[0] + and response_delta.tool_calls[0].function + and response_delta.tool_calls[0].function.arguments is not None + ) + + def _get_named_function_name( + self, request: CreateChatCompletionRequest + ) -> Optional[str]: + if request.tool_choice and isinstance( + request.tool_choice.root, ChatCompletionNamedToolChoice + ): + tool_choice_function_name = request.tool_choice.root.function.name + else: + tool_choice_function_name = None + + if ( + request.tool_choice + and request.tool_choice.root == ChatCompletionToolChoiceOption1.required + ): + tool_choice_required_function_name = request.tools[0].function.name + else: + tool_choice_required_function_name = None + + return tool_choice_function_name or tool_choice_required_function_name diff --git a/python/openai/openai_frontend/engine/utils/chat.py b/python/openai/openai_frontend/engine/utils/chat.py new file mode 100644 index 0000000000..3c40beecc3 --- /dev/null +++ b/python/openai/openai_frontend/engine/utils/chat.py @@ -0,0 +1,165 @@ +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import json +from typing import Dict, Iterable, List, Optional, Required, TypedDict, Union, cast + +# FIXME: Converge on single set of types in either schemas.openai or openai.types +from openai.types.chat import ChatCompletionMessageToolCallParam +from openai.types.chat.chat_completion_message_tool_call_param import Function +from schemas.openai import ( + ChatCompletionMessageToolCall, + ChatCompletionRequestAssistantMessage, + ChatCompletionRequestMessage, + ChatCompletionRequestMessageContentPart, + ChatCompletionRequestToolMessage, + ChatCompletionRequestUserMessage, + Type1, +) + + +class ConversationMessage(TypedDict, total=False): + role: Required[str] + """The role of the message's author.""" + + content: Union[Optional[str], List[Dict[str, str]]] + """The contents of the message""" + + tool_call_id: Optional[str] + """Tool call that this message is responding to.""" + + name: Optional[str] + """The name of the function to call""" + + tool_calls: Optional[Iterable[ChatCompletionMessageToolCallParam]] + """The tool calls generated by the model, such as function calls.""" + + +def _frontend_schema_to_openai_schema_completion_tool_call( + tool_call_param: ChatCompletionMessageToolCall, +) -> ChatCompletionMessageToolCallParam: + return ChatCompletionMessageToolCallParam( + id=tool_call_param.id, + type=tool_call_param.type, + function=Function( + name=tool_call_param.function.name, + arguments=tool_call_param.function.arguments, + ), + ) + + +def _parse_chat_message_content_parts( + role: str, parts: List[ChatCompletionRequestMessageContentPart] +) -> ConversationMessage: + content = list[Dict]() + + for part in parts: + if part.root.type == Type1.text or part.root.type == "text": + parse_res = {"type": "text", "text": part.root.text} + content.append(parse_res) + else: + raise TypeError(f"only text message is supported, but got {part.root.type}") + + return ConversationMessage(role=role, content=content) + + +def _parse_chat_message_content( + message: ChatCompletionRequestMessage, +) -> ConversationMessage: + role = message.root.role + content = message.root.content + + if content is None or isinstance(content, str): + result_msg = ConversationMessage(role=role, content=content) + else: # content is a list of message parts + result_msg = _parse_chat_message_content_parts( + role, + content, + ) + + if role == "assistant": + parsed_msg = cast(ChatCompletionRequestAssistantMessage, message.root) + + if parsed_msg.tool_calls: + result_msg["tool_calls"] = list( + [ + _frontend_schema_to_openai_schema_completion_tool_call(tool_call) + for tool_call in parsed_msg.tool_calls.root + ] + ) + elif role == "tool": + parsed_msg = cast(ChatCompletionRequestToolMessage, message.root) + if parsed_msg.tool_call_id: + result_msg["tool_call_id"] = parsed_msg.tool_call_id + + if isinstance(message.root, ChatCompletionRequestUserMessage) and isinstance( + message.root.name, str + ): + result_msg["name"] = message.root.name + + return result_msg + + +def _postprocess_messages(messages: List[ConversationMessage]) -> None: + # per the Transformers docs & maintainers, tool call arguments in + # assistant-role messages with tool_calls need to be dicts not JSON str - + # this is how tool-use chat templates will expect them moving forwards + # so, for messages that have tool_calls, parse the string (which we get + # from openAI format) to dict + for message in messages: + if ( + message["role"] == "assistant" + and "tool_calls" in message + and isinstance(message["tool_calls"], list) + ): + for item in message["tool_calls"]: + item["function"]["arguments"] = json.loads( + item["function"]["arguments"] + ) + + +def parse_chat_messages( + messages: List[ChatCompletionRequestMessage], +) -> List[ConversationMessage]: + conversation: List[ConversationMessage] = [] + + for msg in messages: + sub_message = _parse_chat_message_content(msg) + conversation.append(sub_message) + + _postprocess_messages(conversation) + + return conversation + + +# This function loads the chat template file content +# if the user chooses to use a chat template different from +# the original one provided with the model's tokenizer. +def load_chat_template(chat_template) -> Optional[str]: + if chat_template is None: + return None + + with open(chat_template) as f: + return f.read() diff --git a/python/openai/openai_frontend/engine/utils/tokenizer.py b/python/openai/openai_frontend/engine/utils/tokenizer.py index 982e553cea..8c21c63773 100644 --- a/python/openai/openai_frontend/engine/utils/tokenizer.py +++ b/python/openai/openai_frontend/engine/utils/tokenizer.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2024-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -32,6 +32,8 @@ from transformers import AutoTokenizer, PreTrainedTokenizer, PreTrainedTokenizerFast +AnyTokenizer = Union[PreTrainedTokenizer, PreTrainedTokenizerFast] + def get_cached_tokenizer( tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] diff --git a/python/openai/openai_frontend/engine/utils/tool_call_parsers/__init__.py b/python/openai/openai_frontend/engine/utils/tool_call_parsers/__init__.py new file mode 100644 index 0000000000..d4bec1e62f --- /dev/null +++ b/python/openai/openai_frontend/engine/utils/tool_call_parsers/__init__.py @@ -0,0 +1,40 @@ +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# Adapted from +# https://github.com/vllm-project/vllm/blob/main/vllm/entrypoints/openai/tool_parsers/__init__.py +# Copyright 2024 The vLLM team. + +from .llama_tool_call_parser import Llama3JsonToolParser +from .mistral_tool_call_parser import MistralToolParser +from .tool_call_parser import ToolCallParser, ToolParserManager + +__all__ = [ + "ToolCallParser", + "ToolParserManager", + "Llama3JsonToolParser", + "MistralToolParser", +] diff --git a/python/openai/openai_frontend/engine/utils/tool_call_parsers/llama_tool_call_parser.py b/python/openai/openai_frontend/engine/utils/tool_call_parsers/llama_tool_call_parser.py new file mode 100644 index 0000000000..ba63189707 --- /dev/null +++ b/python/openai/openai_frontend/engine/utils/tool_call_parsers/llama_tool_call_parser.py @@ -0,0 +1,289 @@ +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# Adapted from +# https://github.com/vllm-project/vllm/blob/main/vllm/entrypoints/openai/tool_parsers/llama_tool_parser.py +# Copyright 2024 The vLLM team. +import json +import uuid +from typing import Union + +import partial_json_parser +from engine.utils.tokenizer import AnyTokenizer +from engine.utils.tool_call_parsers.tool_call_parser import ( + ToolCallParser, + ToolParserManager, +) +from partial_json_parser.core.options import Allow +from schemas.openai import ( + ChatCompletionMessageToolCall, + ChatCompletionMessageToolCallChunk, + ChatCompletionMessageToolCalls, + ChatCompletionResponseMessage, + ChatCompletionStreamResponseDelta, + Function1, + Function2, +) + +from .utils import find_common_prefix, is_complete_json, partial_json_loads + + +@ToolParserManager.register_module("llama3") +class Llama3JsonToolParser(ToolCallParser): + def __init__(self, tokenizer: AnyTokenizer): + super().__init__(tokenizer) + + # initialize properties used for state when parsing tool calls in + # streaming mode + self.prev_tool_call_arr: list[dict] = [] + self.current_tool_id: int = -1 + self.current_tool_name_sent: bool = False + self.streamed_args_for_tool: list[ + str + ] = [] # map what has been streamed for each tool so far to a list + + self.bot_token = "<|python_tag|>" + + def parse_tool_calls( + self, full_text: str, role: str, backend: str + ) -> ChatCompletionResponseMessage: + """ + Extract the tool calls from a complete model response. + """ + # case -- if a tool call token is not present, return a text response + if not (full_text.startswith(self.bot_token) or full_text.startswith("{")): + return ChatCompletionResponseMessage( + tool_calls=None, content=full_text, role=role + ) + + original_full_text = full_text + try: + # FIXME: tensorrt_llm backend might generate some unnecessary text messages + # after the tool call json text starting with "assistant\n\n". + if backend != "vllm": + last_index = full_text.find("assistant\n\n") + if last_index > 0: + full_text = full_text[:last_index] + + # load the JSON, and then use it to build the Function and + # Tool Call + dec = json.JSONDecoder() + function_call_arr = [] + + # depending on the prompt format the Llama model may or may not + # prefix the output with the <|python_tag|> token + start_idx = ( + len(self.bot_token) if full_text.startswith(self.bot_token) else 0 + ) + while start_idx < len(full_text): + (obj, end_idx) = dec.raw_decode(full_text[start_idx:]) + start_idx += end_idx + len("; ") + function_call_arr.append(obj) + + tool_calls = ChatCompletionMessageToolCalls( + root=[ + ChatCompletionMessageToolCall( + id=f"cmpl-{uuid.uuid1()}", + type="function", + function=Function1( + name=raw_function_call["name"], + # function call args are JSON but as a string + arguments=json.dumps( + raw_function_call["arguments"] + if "arguments" in raw_function_call + else raw_function_call["parameters"] + ), + ), + ) + for raw_function_call in function_call_arr + ] + ) + + # get any content before the tool call + ret = ChatCompletionResponseMessage( + tool_calls=tool_calls, content="", role=role + ) + return ret + + except Exception as e: + # return information to just treat the tool call as regular JSON + return ChatCompletionResponseMessage( + tool_calls=None, content=original_full_text, role=role + ) + + def parse_tool_calls_streaming( + self, current_text: str, delta_text: str, backend: str + ) -> Union[ChatCompletionStreamResponseDelta, None]: + if not ( + current_text.startswith(self.bot_token) or current_text.startswith("{") + ): + return ChatCompletionStreamResponseDelta(content=delta_text) + + # bit mask flags for partial JSON parsing. If the name hasn't been + # sent yet, don't allow sending + # an incomplete string since OpenAI only ever (as far as I have + # seen) allows sending the entire tool/ function name at once. + flags = Allow.ALL if self.current_tool_name_sent else Allow.ALL & ~Allow.STR + try: + tool_call_arr = [] + is_complete = [] + try: + # depending on the prompt format the Llama model may or may not + # prefix the output with the <|python_tag|> token + start_idx = ( + len(self.bot_token) + if current_text.startswith(self.bot_token) + else 0 + ) + while start_idx < len(current_text): + (obj, end_idx) = partial_json_loads(current_text[start_idx:], flags) + is_complete.append( + is_complete_json(current_text[start_idx : start_idx + end_idx]) + ) + start_idx += end_idx + len("; ") + # depending on the prompt Llama can use + # either arguments or parameters + if "parameters" in obj: + assert ( + "arguments" not in obj + ), "model generated both parameters and arguments" + obj["arguments"] = obj["parameters"] + tool_call_arr.append(obj) + except partial_json_parser.core.exceptions.MalformedJSON: + return None + + # select as the current tool call the one we're on the state at + current_tool_call: dict = ( + tool_call_arr[self.current_tool_id] if len(tool_call_arr) > 0 else {} + ) + + # case -- if no tokens have been streamed for the tool, e.g. + # only the array brackets, stream nothing + if len(tool_call_arr) == 0: + return None + + # case: we are starting a new tool in the array + # -> array has > 0 length AND length has moved past cursor + elif ( + len(tool_call_arr) > 0 and len(tool_call_arr) > self.current_tool_id + 1 + ): + # if we're moving on to a new call, first make sure we + # haven't missed anything in the previous one that was + # auto-generated due to JSON completions, but wasn't + # streamed to the client yet. + if self.current_tool_id >= 0: + cur_arguments = current_tool_call.get("arguments") + if cur_arguments: + cur_args_json = json.dumps(cur_arguments) + sent = len(self.streamed_args_for_tool[self.current_tool_id]) + argument_diff = cur_args_json[sent:] + + delta = ChatCompletionStreamResponseDelta( + tool_calls=[ + ChatCompletionMessageToolCallChunk( + index=self.current_tool_id, + function=Function2( + arguments=argument_diff + ).model_dump(exclude_none=True), + ) + ] + ) + self.streamed_args_for_tool[ + self.current_tool_id + ] += argument_diff + else: + delta = None + else: + delta = None + # re-set stuff pertaining to progress in the current tool + self.current_tool_id = len(tool_call_arr) - 1 + self.current_tool_name_sent = False + self.streamed_args_for_tool.append("") + return delta + + # if the current tool name hasn't been sent, send if available + # - otherwise send nothing + elif not self.current_tool_name_sent: + function_name = current_tool_call.get("name") + if function_name: + delta = ChatCompletionStreamResponseDelta( + tool_calls=[ + ChatCompletionMessageToolCallChunk( + index=self.current_tool_id, + type="function", + id=f"cmpl-{uuid.uuid1()}", + function=Function2(name=function_name).model_dump( + exclude_none=True + ), + ) + ] + ) + self.current_tool_name_sent = True + else: + delta = None + + # now we know we're on the same tool call and we're streaming + # arguments + else: + cur_arguments = current_tool_call.get("arguments") + delta = None + + if cur_arguments: + sent = len(self.streamed_args_for_tool[self.current_tool_id]) + cur_args_json = json.dumps(cur_arguments) + prev_arguments = self.prev_tool_call_arr[self.current_tool_id].get( + "arguments" + ) + + argument_diff = None + if is_complete[self.current_tool_id]: + argument_diff = cur_args_json[sent:] + elif prev_arguments: + prev_args_json = json.dumps(prev_arguments) + if cur_args_json != prev_args_json: + prefix = find_common_prefix(prev_args_json, cur_args_json) + argument_diff = prefix[sent:] + + if argument_diff is not None: + delta = ChatCompletionStreamResponseDelta( + tool_calls=[ + ChatCompletionMessageToolCallChunk( + index=self.current_tool_id, + function=Function2( + arguments=argument_diff + ).model_dump(exclude_none=True), + ) + ] + ) + self.streamed_args_for_tool[ + self.current_tool_id + ] += argument_diff + + self.prev_tool_call_arr = tool_call_arr + return delta + + except Exception: + return None diff --git a/python/openai/openai_frontend/engine/utils/tool_call_parsers/mistral_tool_call_parser.py b/python/openai/openai_frontend/engine/utils/tool_call_parsers/mistral_tool_call_parser.py new file mode 100644 index 0000000000..e3af3944e9 --- /dev/null +++ b/python/openai/openai_frontend/engine/utils/tool_call_parsers/mistral_tool_call_parser.py @@ -0,0 +1,318 @@ +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# Adapted from +# https://github.com/vllm-project/vllm/blob/main/vllm/entrypoints/openai/tool_parsers/mistral_tool_parser.py +# Copyright 2024 The vLLM team. +import json +import re +from random import choices +from string import ascii_letters, digits +from typing import Dict, List, Union + +import partial_json_parser +from engine.utils.tokenizer import AnyTokenizer +from engine.utils.tool_call_parsers.tool_call_parser import ( + ToolCallParser, + ToolParserManager, +) +from partial_json_parser.core.options import Allow +from schemas.openai import ( + ChatCompletionMessageToolCall, + ChatCompletionMessageToolCallChunk, + ChatCompletionMessageToolCalls, + ChatCompletionResponseMessage, + ChatCompletionStreamResponseDelta, + Function1, + Function2, +) + +from .utils import extract_intermediate_diff + +ALPHANUMERIC = ascii_letters + digits + + +def generate_mistral_random_id(): + # Mistral Tool Call Ids must be alphanumeric with a maximum length of 9. + # https://github.com/mistralai/mistral-common/blob/21ee9f6cee3441e9bb1e6ed2d10173f90bd9b94b/src/mistral_common/protocol/instruct/validator.py#L299 + return "".join(choices(ALPHANUMERIC, k=9)) + + +@ToolParserManager.register_module("mistral") +class MistralToolParser(ToolCallParser): + def __init__(self, tokenizer: AnyTokenizer): + super().__init__(tokenizer) + + # initialize properties used for state when parsing tool calls in + # streaming mode + self.prev_tool_call_arr: List[Dict] = [] + self.current_tool_id: int = -1 + self.current_tool_name_sent: bool = False + self.streamed_args_for_tool: List[ + str + ] = [] # map what has been streamed for each tool so far to a list + self.bot_token = "[TOOL_CALLS]" + self.tool_call_regex = re.compile(r"\[{.*}\]", re.DOTALL) + + def parse_tool_calls( + self, full_text: str, role: str, backend: str + ) -> ChatCompletionResponseMessage: + """ + Extract the tool calls from a complete model response. Requires + find-and-replacing single quotes with double quotes for JSON parsing, + make sure your tool call arguments don't ever include quotes! + """ + + # case -- if a tool call token is not present, return a text response + if not (full_text.startswith(self.bot_token) or full_text.startswith("[")): + return ChatCompletionResponseMessage( + tool_calls=None, content=full_text, role=role + ) + + # first remove the BOT token + tool_content = full_text.replace(self.bot_token, "").strip() + try: + # we first try to directly load the json as parsing very nested + # jsons is difficult + try: + function_call_arr = json.loads(tool_content) + except json.JSONDecodeError: + # use a regex to find the part corresponding to the tool call. + # NOTE: This use case should not happen if the model is trained + # correctly. It's a easy possible fix so it's included, but + # can be brittle for very complex / highly nested tool calls + raw_tool_call = self.tool_call_regex.findall(tool_content)[0] + function_call_arr = json.loads(raw_tool_call) + + # Tool Call + tool_calls = ChatCompletionMessageToolCalls( + root=[ + ChatCompletionMessageToolCall( + id=generate_mistral_random_id(), + type="function", + function=Function1( + name=raw_function_call["name"], + # function call args are JSON but as a string + arguments=json.dumps( + raw_function_call["arguments"], ensure_ascii=False + ), + ), + ) + for raw_function_call in function_call_arr + ] + ) + + # get any content before the tool call + content = ( + full_text.split(self.bot_token)[0] + if full_text.startswith(self.bot_token) + else "" + ) + return ChatCompletionResponseMessage( + tool_calls=tool_calls, content=content, role=role + ) + + except Exception: + # return information to just treat the tool call as regular JSON + return ChatCompletionResponseMessage( + tool_calls=None, content=full_text, role=role + ) + + def parse_tool_calls_streaming( + self, current_text: str, delta_text: str, backend: str + ) -> Union[ChatCompletionStreamResponseDelta, None]: + # if the tool call token is not in the tokens generated so far, append + # output to contents since it's not a tool + # tensorrt_llm backend likely doesn't generate the bos token + if not (self.bot_token in current_text or "[" in current_text): + return ChatCompletionStreamResponseDelta(content=delta_text) + + # handle if we detected the BOT token which means the start of tool + # calling + if self.bot_token == delta_text.strip(): + # if it's the only token, return None, so we don't send a chat + # completion any don't send a control token + return None + + flags = Allow.ALL if self.current_tool_name_sent else Allow.ALL & ~Allow.STR + + try: + # replace BOT token with empty string, and convert single quotes + # to double to allow parsing as JSON since mistral uses single + # quotes instead of double for tool calls + parsable_arr = current_text.split(self.bot_token)[-1] + + # tool calls are generated in an array, so do partial JSON + # parsing on the entire array + try: + tool_call_arr: List[Dict] = partial_json_parser.loads( + parsable_arr, flags + ) + except partial_json_parser.core.exceptions.MalformedJSON: + return None + + # select as the current tool call the one we're on the state at + + current_tool_call: Dict = ( + tool_call_arr[self.current_tool_id] if len(tool_call_arr) > 0 else {} + ) + + # case -- if no tokens have been streamed for the tool, e.g. + # only the array brackets, stream nothing + if len(tool_call_arr) == 0: + return None + + # case: we are starting a new tool in the array + # -> array has > 0 length AND length has moved past cursor + elif ( + len(tool_call_arr) > 0 and len(tool_call_arr) > self.current_tool_id + 1 + ): + # if we're moving on to a new call, first make sure we + # haven't missed anything in the previous one that was + # auto-generated due to JSON completions, but wasn't + # streamed to the client yet. + if self.current_tool_id >= 0: + diff: Union[str, None] = current_tool_call.get("arguments") + + if diff: + diff = json.dumps(diff, ensure_ascii=False).replace( + self.streamed_args_for_tool[self.current_tool_id], "" + ) + + delta = ChatCompletionStreamResponseDelta( + tool_calls=[ + ChatCompletionMessageToolCallChunk( + index=self.current_tool_id, + function=Function2(arguments=diff).model_dump( + exclude_none=True + ), + ) + ] + ) + self.streamed_args_for_tool[self.current_tool_id] += diff + else: + delta = None + else: + delta = None + # re-set stuff pertaining to progress in the current tool + self.current_tool_id = len(tool_call_arr) - 1 + self.current_tool_name_sent = False + self.streamed_args_for_tool.append("") + return delta + + # case: update an existing tool - this is handled below + + # if the current tool name hasn't been sent, send if available + # - otherwise send nothing + if not self.current_tool_name_sent: + function_name = current_tool_call.get("name") + if function_name: + delta = ChatCompletionStreamResponseDelta( + tool_calls=[ + ChatCompletionMessageToolCallChunk( + index=self.current_tool_id, + type="function", + id=generate_mistral_random_id(), + function=Function2(name=function_name).model_dump( + exclude_none=True + ), + ) + ] + ) + self.current_tool_name_sent = True + else: + delta = None + + # now we know we're on the same tool call and we're streaming + # arguments + else: + prev_arguments = self.prev_tool_call_arr[self.current_tool_id].get( + "arguments" + ) + cur_arguments = current_tool_call.get("arguments") + + new_text = delta_text.replace("'", '"') + if '"}' in new_text: + new_text = new_text[: new_text.rindex('"}')] + + if not cur_arguments and not prev_arguments: + delta = None + elif not cur_arguments and prev_arguments: + delta = None + elif cur_arguments and not prev_arguments: + cur_arguments_json = json.dumps(cur_arguments, ensure_ascii=False)[ + :-2 + ] + + if new_text not in cur_arguments_json: + return None + arguments_delta = cur_arguments_json[ + : cur_arguments_json.rindex(new_text) + len(new_text) + ] + delta = ChatCompletionStreamResponseDelta( + tool_calls=[ + ChatCompletionMessageToolCallChunk( + index=self.current_tool_id, + function=Function2( + arguments=arguments_delta + ).model_dump(exclude_none=True), + ) + ] + ) + self.streamed_args_for_tool[self.current_tool_id] += arguments_delta + + elif cur_arguments and prev_arguments: + cur_args_json = json.dumps(cur_arguments, ensure_ascii=False) + prev_args_json = json.dumps(prev_arguments, ensure_ascii=False) + + argument_diff = extract_intermediate_diff( + cur_args_json, prev_args_json + ) + delta = ChatCompletionStreamResponseDelta( + tool_calls=[ + ChatCompletionMessageToolCallChunk( + index=self.current_tool_id, + function=Function2(arguments=argument_diff).model_dump( + exclude_none=True + ), + ) + ] + ) + self.streamed_args_for_tool[self.current_tool_id] += argument_diff + else: + # try parsing it with regular JSON - if it works we're + # at the end, and we need to send the difference between + # tokens streamed so far and the valid JSON + delta = None + + # check to see if the name is defined and has been sent. if so, + # stream the name - otherwise keep waiting + # finish by setting old and returning None as base case + self.prev_tool_call_arr = tool_call_arr + return delta + + except Exception: + return None diff --git a/python/openai/openai_frontend/engine/utils/tool_call_parsers/tool_call_parser.py b/python/openai/openai_frontend/engine/utils/tool_call_parsers/tool_call_parser.py new file mode 100644 index 0000000000..5ae192eb6c --- /dev/null +++ b/python/openai/openai_frontend/engine/utils/tool_call_parsers/tool_call_parser.py @@ -0,0 +1,133 @@ +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# Adapted from +# https://github.com/vllm-project/vllm/blob/main/vllm/entrypoints/openai/tool_parsers/abstract_tool_parser.py +# Copyright 2024 The vLLM team. +from typing import Callable, Dict, List, Optional, Union + +from engine.utils.tokenizer import AnyTokenizer +from schemas.openai import ( + ChatCompletionMessageToolCalls, + ChatCompletionStreamResponseDelta, +) + + +class ToolCallParser: + """The Base Tool Call Parser for parsing the Tool Call from the responses, + Two inferfaces are supported: the one-time parser for synchronized response + and streaming parser for streaming response. + """ + + def __init__(self, tokenizer: AnyTokenizer): + self.prev_tool_call_arr: List[Dict] = [] + # the index of the tool call that is currently being parsed + self.current_tool_id: int = -1 + self.current_tool_name_sent: bool = False + self.streamed_args_for_tool: List[str] = [] + + self.model_tokenizer = tokenizer + + def parse_tool_calls( + self, full_text: str, role: str, backend: str + ) -> ChatCompletionMessageToolCalls: + raise NotImplementedError( + "BaseToolCallParser.parse_tool_calls has not been implemented!" + ) + + def parse_tool_calls_streaming( + self, current_text: str, delta_text: str, backend: str + ) -> ChatCompletionStreamResponseDelta: + raise NotImplementedError( + "BaseToolCallParser.parse_tool_calls_streaming has not been implemented!" + ) + + +class ToolParserManager: + tool_parsers: dict[str, type] = {} + + @classmethod + def get_tool_parser_cls(cls, name) -> type: + if name in cls.tool_parsers: + return cls.tool_parsers[name] + + raise KeyError(f"tool parser: '{name}' not found in tool_call_parsers") + + @classmethod + def _register_module( + cls, + module: type, + module_name: Optional[Union[str, list[str]]] = None, + force: bool = True, + ) -> None: + if not issubclass(module, ToolCallParser): + raise TypeError( + f"module must be subclass of ToolCallParser, but got {type(module)}" + ) + if module_name is None: + module_name = module.__name__ + if isinstance(module_name, str): + module_name = [module_name] + for name in module_name: + if not force and name in cls.tool_parsers: + existed_module = cls.tool_parsers[name] + raise KeyError( + f"{name} is already registered " f"at {existed_module.__module__}" + ) + cls.tool_parsers[name] = module + + @classmethod + def register_module( + cls, + name: Optional[Union[str, list[str]]] = None, + force: bool = True, + module: Union[type, None] = None, + ) -> Union[type, Callable]: + """ + Register module with the given name or name list. it can be used as a + decoder(with module as None) or normal function(with module as not + None). + """ + if not isinstance(force, bool): + raise TypeError(f"force must be a boolean, but got {type(force)}") + + # raise the error ahead of time + if not (name is None or isinstance(name, str)): + raise TypeError( + "name must be None, an instance of str, " f"but got {type(name)}" + ) + + # use it as a normal method: x.register_module(module=SomeClass) + if module is not None: + cls._register_module(module=module, module_name=name, force=force) + return module + + # use it as a decorator: @x.register_module() + def _register(module): + cls._register_module(module=module, module_name=name, force=force) + return module + + return _register diff --git a/python/openai/openai_frontend/engine/utils/tool_call_parsers/utils.py b/python/openai/openai_frontend/engine/utils/tool_call_parsers/utils.py new file mode 100644 index 0000000000..60513d3f8f --- /dev/null +++ b/python/openai/openai_frontend/engine/utils/tool_call_parsers/utils.py @@ -0,0 +1,129 @@ +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# Adapted from +# https://github.com/vllm-project/vllm/blob/main/vllm/entrypoints/openai/tool_parsers/utils.py +# Copyright 2024 The vLLM team. + +import json +from typing import Any + +import partial_json_parser +from partial_json_parser.core.options import Allow + + +# partial_json_parser doesn't support extra data and +# JSONDecorder.raw_decode doesn't support partial JSON +def partial_json_loads(input_str: str, flags: Allow) -> tuple[Any, int]: + try: + return (partial_json_parser.loads(input_str, flags), len(input_str)) + except json.JSONDecodeError as e: + if "Extra data" in e.msg: + dec = json.JSONDecoder() + return dec.raw_decode(input_str) + raise + + +def is_complete_json(input_str: str) -> bool: + try: + json.loads(input_str) + return True + except json.JSONDecodeError: + return False + + +def find_common_prefix(s1: str, s2: str) -> str: + """ + Finds a common prefix that is shared between two strings, if there is one. + Order of arguments is NOT important. + + This function is provided as a UTILITY for extracting information from JSON + generated by partial_json_parser, to help in ensuring that the right tokens + are returned in streaming, so that close-quotes, close-brackets and + close-braces are not returned prematurely. + + e.g. find_common_prefix('{"fruit": "ap"}', '{"fruit": "apple"}') -> + '{"fruit": "ap' + """ + prefix = "" + min_length = min(len(s1), len(s2)) + for i in range(0, min_length): + if s1[i] == s2[i]: + prefix += s1[i] + else: + break + return prefix + + +def find_common_suffix(s1: str, s2: str) -> str: + """ + Finds a common suffix shared between two strings, if there is one. Order of + arguments is NOT important. + Stops when the suffix ends OR it hits an alphanumeric character + + e.g. find_common_suffix('{"fruit": "ap"}', '{"fruit": "apple"}') -> '"}' + """ + suffix = "" + min_length = min(len(s1), len(s2)) + for i in range(1, min_length + 1): + if s1[-i] == s2[-i] and not s1[-i].isalnum(): + suffix = s1[-i] + suffix + else: + break + return suffix + + +def extract_intermediate_diff(curr: str, old: str) -> str: + """ + Given two strings, extract the difference in the middle between two strings + that are known to have a common prefix and/or suffix. + + This function is provided as a UTILITY for extracting information from JSON + generated by partial_json_parser, to help in ensuring that the right tokens + are returned in streaming, so that close-quotes, close-brackets and + close-braces are not returned prematurely. The order of arguments IS + important - the new version of the partially-parsed JSON must be the first + argument, and the second argument must be from the previous generation. + + What it returns, is tokens that should be streamed to the client. + + e.g. extract_intermediate_diff('{"fruit": "apple"}', '{"fruit": "ap"}') + -> 'ple' + + """ + suffix = find_common_suffix(curr, old) + + old = old[::-1].replace(suffix[::-1], "", 1)[::-1] + prefix = find_common_prefix(curr, old) + diff = curr + if len(suffix): + diff = diff[::-1].replace(suffix[::-1], "", 1)[::-1] + + if len(prefix): + # replace the prefix only once in case it's mirrored + diff = diff.replace(prefix, "", 1) + + return diff diff --git a/python/openai/openai_frontend/engine/utils/triton.py b/python/openai/openai_frontend/engine/utils/triton.py index 438890a05a..ed16c2c1b8 100644 --- a/python/openai/openai_frontend/engine/utils/triton.py +++ b/python/openai/openai_frontend/engine/utils/triton.py @@ -23,16 +23,22 @@ # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - import ctypes import json import os import re -from typing import Iterable, List +from dataclasses import asdict +from typing import Iterable, List, Optional, Union import numpy as np import tritonserver -from schemas.openai import CreateChatCompletionRequest, CreateCompletionRequest +from pydantic import BaseModel +from schemas.openai import ( + ChatCompletionNamedToolChoice, + ChatCompletionToolChoiceOption1, + CreateChatCompletionRequest, + CreateCompletionRequest, +) def _create_vllm_inference_request( @@ -73,6 +79,16 @@ def _create_vllm_inference_request( sampling_parameters["lora_name"] = lora_name sampling_parameters = json.dumps(sampling_parameters) + guided_json = _get_guided_json_from_tool(request) + if guided_json is not None: + from vllm.sampling_params import GuidedDecodingParams + + sampling_parameters_json = json.loads(sampling_parameters) + sampling_parameters_json["guided_decoding"] = json.dumps( + asdict(GuidedDecodingParams.from_optional(json=guided_json)) + ) + sampling_parameters = json.dumps(sampling_parameters_json) + exclude_input_in_output = True echo = getattr(request, "echo", None) if echo is not None: @@ -116,6 +132,11 @@ def _create_trtllm_inference_request( inputs["random_seed"] = np.uint64([[request.seed]]) if request.temperature is not None: inputs["temperature"] = np.float32([[request.temperature]]) + + guided_json = _get_guided_json_from_tool(request) + if guided_json is not None: + inputs["guided_decoding_guide_type"] = [["json_schema"]] + inputs["guided_decoding_guide"] = [[guided_json]] # FIXME: TRT-LLM doesn't currently support runtime changes of 'echo' and it # is configured at model load time, so we don't handle it here for now. return model.create_request(inputs=inputs) @@ -190,6 +211,29 @@ def _validate_triton_responses_non_streaming( raise Exception(f"Unexpected number of responses: {num_responses}, expected 1.") +def _get_guided_json_from_tool( + request: CreateChatCompletionRequest | CreateCompletionRequest, +) -> Optional[Union[str, dict, BaseModel]]: + if isinstance(request, CreateChatCompletionRequest): + if request.tool_choice is None or not request.tools: + return None + + if type(request.tool_choice.root) is ChatCompletionNamedToolChoice: + tool_name = request.tool_choice.root.function.name + elif request.tool_choice.root == ChatCompletionToolChoiceOption1.required: + tool_name = request.tools[0].function.name + else: + return None + + tools = {tool.function.name: tool.function for tool in request.tools} + if tool_name not in tools: + raise ValueError(f"Tool '{tool_name}' has not been passed in `tools`.") + tool = tools[tool_name] + return tool.parameters.model_dump_json() + + return None + + def _get_vllm_lora_names( model_repository: str | list[str], model_name: str, model_version: int ) -> None | List[str]: diff --git a/python/openai/openai_frontend/main.py b/python/openai/openai_frontend/main.py index d8899875c4..8de1f8c2f2 100755 --- a/python/openai/openai_frontend/main.py +++ b/python/openai/openai_frontend/main.py @@ -126,6 +126,22 @@ def parse_args(): default="0.0.0.0", help="Address/host of frontends (default: '0.0.0.0')", ) + triton_group.add_argument( + "--tool-call-parser", + type=str, + default=None, + help="Specify the parser for handling tool calling related response text. Options include: 'llama3' and 'mistral'.", + ) + # Allows the user to try a different chat template to craft better prompts and receive more targeted tool-calling responses from the model. + # Some Mistral models have a separate chat template file, in addition to the tokenizer_config.json, + # such as mistralai/Mistral-Small-3.1-24B-Instruct-2503. + # This can serve as a workaround for those models. + triton_group.add_argument( + "--chat-template", + type=str, + default=None, + help="The path to the custom Jinja chat template file. This is useful if you'd like to use a different chat template than the one provided by the model.", + ) # OpenAI-Compatible Frontend (FastAPI) openai_group = parser.add_argument_group("Triton OpenAI-Compatible Frontend") @@ -181,6 +197,8 @@ def main(): tokenizer=args.tokenizer, backend=args.backend, lora_separator=args.lora_separator, + tool_call_parser=args.tool_call_parser, + chat_template=args.chat_template, ) # Attach TritonLLMEngine as the backbone for inference and model management diff --git a/python/openai/openai_frontend/schemas/openai.py b/python/openai/openai_frontend/schemas/openai.py index 8b5545d910..a2438e8394 100644 --- a/python/openai/openai_frontend/schemas/openai.py +++ b/python/openai/openai_frontend/schemas/openai.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2024-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -320,10 +320,6 @@ class ChatCompletionFunctionCallOption(BaseModel): name: str = Field(..., description="The name of the function to call.") -class Type2(Enum): - function = "function" - - class FunctionObject(BaseModel): description: Optional[str] = Field( None, @@ -347,7 +343,7 @@ class Function(BaseModel): class ChatCompletionNamedToolChoice(BaseModel): - type: Type2 = Field( + type: str = Field( ..., description="The type of the tool. Currently, only `function` is supported.", ) @@ -364,7 +360,7 @@ class Function1(BaseModel): class ChatCompletionMessageToolCall(BaseModel): id: str = Field(..., description="The ID of the tool call.") - type: Type2 = Field( + type: str = Field( ..., description="The type of the tool. Currently, only `function` is supported.", ) @@ -382,7 +378,7 @@ class Function2(BaseModel): class ChatCompletionMessageToolCallChunk(BaseModel): index: int id: Optional[str] = Field(None, description="The ID of the tool call.") - type: Optional[Type2] = Field( + type: Optional[str] = Field( None, description="The type of the tool. Currently, only `function` is supported.", ) @@ -683,7 +679,7 @@ class ChatCompletionRequestUserMessage(BaseModel): class ChatCompletionTool(BaseModel): - type: Type2 = Field( + type: str = Field( ..., description="The type of the tool. Currently, only `function` is supported.", ) diff --git a/python/openai/requirements.txt b/python/openai/requirements.txt index ac84944631..5536880c09 100644 --- a/python/openai/requirements.txt +++ b/python/openai/requirements.txt @@ -30,6 +30,7 @@ fastapi==0.115.6 # https://community.openai.com/t/error-with-openai-1-56-0-client-init-got-an-unexpected-keyword-argument-proxies/1040332/3 httpx==0.27.2 openai==1.60.0 +partial-json-parser # used for parsing partial JSON outputs # Minimum starlette version needed to address CVE: # https://github.com/advisories/GHSA-f96h-pmfr-66vw starlette>=0.40.0 diff --git a/python/openai/tests/conftest.py b/python/openai/tests/conftest.py index 9ea9a5634e..50ba0de4ed 100644 --- a/python/openai/tests/conftest.py +++ b/python/openai/tests/conftest.py @@ -1,4 +1,4 @@ -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -33,13 +33,16 @@ ### TEST ENVIRONMENT SETUP ### -def infer_test_environment(): +def infer_test_environment(tool_call_parser): # Infer the test environment for simplicity in local dev/testing. try: import vllm as _ backend = "vllm" - model = "llama-3.1-8b-instruct" + if tool_call_parser == "mistral": + model = "mistral-nemo-instruct-2407" + else: + model = "llama-3.1-8b-instruct" return backend, model except ImportError: print("No vllm installation found.") @@ -56,8 +59,11 @@ def infer_test_environment(): raise Exception("Unknown test environment") -def infer_test_model_repository(backend): - model_repository = str(Path(__file__).parent / f"{backend}_models") +def infer_test_model_repository(backend, tool_call_parser): + if tool_call_parser == "mistral": + model_repository = str(Path(__file__).parent / f"{backend}_mistral_models") + else: + model_repository = str(Path(__file__).parent / f"{backend}_models") return model_repository @@ -69,14 +75,17 @@ def infer_test_model_repository(backend): TEST_TOKENIZER = os.environ.get( "TEST_TOKENIZER", "meta-llama/Meta-Llama-3.1-8B-Instruct" ) +TEST_TOOL_CALL_PARSER = os.environ.get("TEST_TOOL_CALL_PARSER", "llama3") TEST_PROMPT = "What is machine learning?" TEST_MESSAGES = [{"role": "user", "content": TEST_PROMPT}] if not TEST_BACKEND or not TEST_MODEL: - TEST_BACKEND, TEST_MODEL = infer_test_environment() + TEST_BACKEND, TEST_MODEL = infer_test_environment(TEST_TOOL_CALL_PARSER) if not TEST_MODEL_REPOSITORY: - TEST_MODEL_REPOSITORY = infer_test_model_repository(TEST_BACKEND) + TEST_MODEL_REPOSITORY = infer_test_model_repository( + TEST_BACKEND, TEST_TOOL_CALL_PARSER + ) # NOTE: OpenAI client requires actual server running, and won't work @@ -91,6 +100,8 @@ def server(): TEST_TOKENIZER, "--backend", TEST_BACKEND, + "--tool-call-parser", + TEST_TOOL_CALL_PARSER, ] # TODO: Incorporate kserve frontend binding smoke tests to catch any # breakage with default values or slight cli arg variations @@ -145,3 +156,27 @@ def prompt(): @pytest.fixture(scope="module") def messages(): return TEST_MESSAGES + + +# FIXME: In TRTLLM tests, the in-process Triton server for the FastAPI app +# does not automatically release GPU memory, even after calling stop(). +# The memory is only released when the entire pytest process exits. +# +# As a result, when the OpenAI server starts another Triton server as a subprocess, +# there may not be enough GPU memory available to launch a new model instance. +# +# This is a workaround to ensure that tests using the OpenAI server run first. +# Once the OpenAI server subprocess is terminated, tests using the FastAPI app can safely run. +def pytest_collection_modifyitems(session, config, items): + def get_priority(item): + cls = item.cls + if cls: + if getattr(cls, "pytestmark", None): + for mark in cls.pytestmark: + if mark.name == "openai": + return 0 + elif mark.name == "fastapi": + return 1 + return 2 # unmarked tests last + + items.sort(key=get_priority) diff --git a/python/openai/tests/test_chat_completions.py b/python/openai/tests/test_chat_completions.py index 401601c526..532d898788 100644 --- a/python/openai/tests/test_chat_completions.py +++ b/python/openai/tests/test_chat_completions.py @@ -1,4 +1,4 @@ -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -35,6 +35,7 @@ from tests.utils import setup_fastapi_app, setup_server +@pytest.mark.fastapi class TestChatCompletions: @pytest.fixture(scope="class") def client(self, fastapi_client_class_scope): @@ -503,6 +504,7 @@ def test_usage_response(self): # For tests that won't use the same pytest fixture for server startup across # the whole class test suite. +@pytest.mark.fastapi class TestChatCompletionsTokenizers: # Re-use a single Triton server for different frontend configurations @pytest.fixture(scope="class") diff --git a/python/openai/tests/test_completions.py b/python/openai/tests/test_completions.py index d89ff4701e..1a411a0124 100644 --- a/python/openai/tests/test_completions.py +++ b/python/openai/tests/test_completions.py @@ -1,4 +1,4 @@ -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -29,6 +29,7 @@ import pytest +@pytest.mark.fastapi class TestCompletions: @pytest.fixture(scope="class") def client(self, fastapi_client_class_scope): diff --git a/python/openai/tests/test_openai_client.py b/python/openai/tests/test_openai_client.py index 6f1b456ab4..618e4052ed 100644 --- a/python/openai/tests/test_openai_client.py +++ b/python/openai/tests/test_openai_client.py @@ -1,4 +1,4 @@ -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -30,6 +30,7 @@ import pytest +@pytest.mark.openai class TestOpenAIClient: @pytest.fixture(scope="class") def client(self, server): @@ -94,6 +95,7 @@ def test_openai_client_function_calling(self): pass +@pytest.mark.openai class TestAsyncOpenAIClient: @pytest.fixture(scope="class") def client(self, server): @@ -243,8 +245,3 @@ async def test_chat_streaming( assert len(chunks) > 1 streamed_output = "".join(chunks) assert streamed_output == output - - @pytest.mark.skip(reason="Not Implemented Yet") - @pytest.mark.asyncio - async def test_openai_client_function_calling(self): - pass diff --git a/python/openai/tests/test_tool_calling.py b/python/openai/tests/test_tool_calling.py new file mode 100644 index 0000000000..9299e4a036 --- /dev/null +++ b/python/openai/tests/test_tool_calling.py @@ -0,0 +1,592 @@ +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import json +import os +from typing import Dict, List, Optional + +import openai +import pytest +from openai.types.chat import ( + ChatCompletionMessageParam, + ChatCompletionMessageToolCall, + ChatCompletionNamedToolChoiceParam, + ChatCompletionToolParam, +) + +# resources for testing the tool callings +WEATHER_TOOL: ChatCompletionToolParam = { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "city": { + "type": "string", + "description": "The city to find the weather for, " + "e.g. 'San Francisco'", + }, + "state": { + "type": "string", + "description": "must the two-letter abbreviation for the state " + "that the city is in, e.g. 'CA' which would " + "mean 'California'", + }, + "unit": { + "type": "string", + "description": "The unit to fetch the temperature in", + "enum": ["celsius", "fahrenheit"], + }, + }, + "required": ["city", "state", "unit"], + }, + }, +} + +WEATHER_FORECAST_TOOL: ChatCompletionToolParam = { + "type": "function", + "function": { + "name": "get_n_day_weather_forecast", + "description": "Get an N-day weather forecast", + "parameters": { + "type": "object", + "properties": { + "city": { + "type": "string", + "description": "The city to find the weather for, " + "e.g. 'San Francisco'", + }, + "state": { + "type": "string", + "description": "must the two-letter abbreviation for the state " + "that the city is in, e.g. 'CA' which would " + "mean 'California'", + }, + "unit": { + "type": "string", + "description": "The unit to fetch the temperature in", + "enum": ["celsius", "fahrenheit"], + }, + "num_days": { + "type": "integer", + "description": "The number of days to forecast", + }, + }, + "required": ["city", "state", "unit", "num_days"], + }, + }, +} + +MESSAGES_ASKING_FOR_TOOLS: List[ChatCompletionMessageParam] = [ + { + "role": "system", + "content": "You're a helpful assistant! Answer the users question best you can.", + }, + {"role": "user", "content": "What is the weather in Dallas, Texas in Fahrenheit?"}, +] + +MESSAGES_WITH_TOOL_RESPONSE: List[ChatCompletionMessageParam] = [ + { + "role": "system", + "content": "You're a helpful assistant! Answer the users question best you can.", + }, + {"role": "user", "content": "What is the weather in Dallas, Texas in Fahrenheit?"}, + { + "role": "assistant", + "tool_calls": [ + { + "id": "123456789", + "type": "function", + "function": { + "name": "get_current_weather", + "arguments": '{"city": "Dallas", "state": "TX", ' + '"unit": "fahrenheit"}', + }, + } + ], + }, + {"role": "tool", "tool_call_id": "123456789", "content": "98"}, +] + +WEATHER_FORECAST_TOOL_CHOICE: ChatCompletionNamedToolChoiceParam = { + "function": {"name": "get_n_day_weather_forecast"}, + "type": "function", +} + + +@pytest.mark.openai +class TestAsyncClientToolCalling: + @pytest.fixture(scope="class") + def client(self, server): + return server.get_async_client() + + def validate_tool_calls_present( + self, tool_calls: Optional[List[ChatCompletionMessageToolCall]], skip_id=False + ): + assert tool_calls is not None + assert len(tool_calls) == 1 + assert tool_calls[0].type == "function" + assert tool_calls[0].function is not None + assert isinstance(tool_calls[0].id, str) + if not skip_id: + assert len(tool_calls[0].id) >= 9 + + def validate_weather_tool_arguments(self, parsed_arguments: Dict): + assert isinstance(parsed_arguments, Dict) + assert isinstance(parsed_arguments.get("city"), str) + assert isinstance(parsed_arguments.get("state"), str) + assert isinstance(parsed_arguments.get("unit"), str) + assert parsed_arguments.get("city") == "Dallas" + assert parsed_arguments.get("state") in ("TX", "Texas") + assert parsed_arguments.get("unit") == "fahrenheit" + + def validate_weather_forcast_tool_arguments(self, parsed_arguments: Dict): + assert isinstance(parsed_arguments, Dict) + assert isinstance(parsed_arguments.get("city"), str) + assert isinstance(parsed_arguments.get("state"), str) + assert isinstance(parsed_arguments.get("unit"), str) + assert isinstance(parsed_arguments.get("num_days"), int) + assert parsed_arguments.get("city") == "Dallas" + assert parsed_arguments.get("state") in ("TX", "Texas") + assert parsed_arguments.get("unit") == "fahrenheit" + + @pytest.mark.asyncio + async def test_tool_call_and_choice(self, client: openai.AsyncOpenAI, model: str): + chat_completion = await client.chat.completions.create( + messages=MESSAGES_ASKING_FOR_TOOLS, + temperature=0, + max_tokens=128, + model=model, + tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], + logprobs=False, + ) + + choice = chat_completion.choices[0] + stop_reason = chat_completion.choices[0].finish_reason + tool_calls = chat_completion.choices[0].message.tool_calls + + # make sure a tool call is present + self.validate_tool_calls_present(tool_calls) + assert stop_reason == "tool_calls" + + # make sure the weather tool was called (classic example) with arguments + assert tool_calls[0].function.name == WEATHER_TOOL["function"]["name"] + assert tool_calls[0].function.arguments is not None + assert isinstance(tool_calls[0].function.arguments, str) + + # make sure the arguments parse properly + parsed_arguments = json.loads(tool_calls[0].function.arguments) + self.validate_weather_tool_arguments(parsed_arguments) + + function_name: Optional[str] = None + function_args_str: str = "" + tool_call_id: Optional[str] = None + role_name: Optional[str] = None + finish_reason_count: int = 0 + + # make the same request, streaming + stream = await client.chat.completions.create( + model=model, + messages=MESSAGES_ASKING_FOR_TOOLS, + temperature=0, + max_tokens=128, + tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], + logprobs=False, + stream=True, + ) + + async for chunk in stream: + assert chunk.choices[0].index == 0 + + if chunk.choices[0].finish_reason: + finish_reason_count += 1 + assert chunk.choices[0].finish_reason == "tool_calls" + + # if a role is being streamed make sure it wasn't already set to + # something else + if chunk.choices[0].delta.role: + assert not role_name or role_name == "assistant" + role_name = "assistant" + + # if a tool call is streamed make sure there's exactly one + # (based on the request parameters + streamed_tool_calls = chunk.choices[0].delta.tool_calls + + if streamed_tool_calls and len(streamed_tool_calls) > 0: + assert len(streamed_tool_calls) == 1 + tool_call = streamed_tool_calls[0] + + # if a tool call ID is streamed, make sure one hasn't been already + if tool_call.id: + assert not tool_call_id + tool_call_id = tool_call.id + + # if parts of the function start being streamed + if tool_call.function: + # if the function name is defined, set it. it should be streamed + # IN ENTIRETY, exactly one time. + if tool_call.function.name: + assert function_name is None + assert isinstance(tool_call.function.name, str) + function_name = tool_call.function.name + if tool_call.function.arguments: + assert isinstance(tool_call.function.arguments, str) + function_args_str += tool_call.function.arguments + + assert finish_reason_count == 1 + assert role_name == "assistant" + assert isinstance(tool_call_id, str) and (len(tool_call_id) >= 9) + + # validate the name and arguments + assert function_name == WEATHER_TOOL["function"]["name"] + assert function_name == tool_calls[0].function.name + assert isinstance(function_args_str, str) + + # validate arguments + streamed_args = json.loads(function_args_str) + self.validate_weather_tool_arguments(streamed_args) + + # make sure everything matches non-streaming except for ID + assert function_name == tool_calls[0].function.name + assert choice.message.role == role_name + assert choice.message.tool_calls[0].function.name == function_name + + # compare streamed with non-streamed args Dict-wise, not string-wise + # because character-to-character comparison might not work e.g. the tool + # call parser adding extra spaces or something like that. we care about the + # dicts matching not byte-wise match + assert parsed_arguments == streamed_args + + @pytest.mark.asyncio + async def test_tool_call_with_reply_response( + self, client: openai.AsyncOpenAI, model: str, backend: str + ): + chat_completion = await client.chat.completions.create( + messages=MESSAGES_WITH_TOOL_RESPONSE, + temperature=0, + max_tokens=128, + model=model, + tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], + logprobs=False, + ) + + choice = chat_completion.choices[0] + + assert choice.finish_reason != "tool_calls" # "stop" + assert choice.message.role == "assistant" + assert choice.message.tool_calls is None or len(choice.message.tool_calls) == 0 + assert choice.message.content is not None + + stream = await client.chat.completions.create( + messages=MESSAGES_WITH_TOOL_RESPONSE, + temperature=0, + max_tokens=128, + model=model, + tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], + logprobs=False, + stream=True, + ) + + chunks: List[str] = [] + finish_reason_count = 0 + role_sent: bool = False + + async for chunk in stream: + delta = chunk.choices[0].delta + + if delta.role: + assert not role_sent + assert delta.role == "assistant" + role_sent = True + + if delta.content: + chunks.append(delta.content) + + if chunk.choices[0].finish_reason is not None: + finish_reason_count += 1 + assert chunk.choices[0].finish_reason == choice.finish_reason + + assert not delta.tool_calls or len(delta.tool_calls) == 0 + + assert role_sent + assert finish_reason_count == 1 + assert len(chunks) + + # validate if steaming and non-streaming generates the same content + assert "".join(chunks) == choice.message.content + + @pytest.mark.skipif( + os.environ.get("IMAGE_KIND") == "TRTLLM", + reason="latest release version of Tensorrt LLM 0.18 doesn't support guided decoding", + ) + @pytest.mark.asyncio + async def test_tool_call_with_named_tool_choice( + self, client: openai.AsyncOpenAI, model: str + ): + chat_completion = await client.chat.completions.create( + messages=MESSAGES_ASKING_FOR_TOOLS, + temperature=0, + max_tokens=128, + model=model, + tool_choice=WEATHER_FORECAST_TOOL_CHOICE, + tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], + logprobs=False, + ) + + choice = chat_completion.choices[0] + stop_reason = chat_completion.choices[0].finish_reason + tool_calls = chat_completion.choices[0].message.tool_calls + + # make sure a tool call is present + self.validate_tool_calls_present(tool_calls, skip_id=True) + assert stop_reason != "tool_calls" + + # make sure the weather tool was called (classic example) with arguments + assert tool_calls[0].function.name == WEATHER_FORECAST_TOOL["function"]["name"] + assert tool_calls[0].function.arguments is not None + assert isinstance(tool_calls[0].function.arguments, str) + + # make sure the arguments parse properly + parsed_arguments = json.loads(tool_calls[0].function.arguments) + self.validate_weather_forcast_tool_arguments(parsed_arguments) + + function_name: Optional[str] = None + function_args_str: str = "" + tool_call_id: Optional[str] = None + role_name: Optional[str] = None + finish_reason_count: int = 0 + + # make the same request, streaming + stream = await client.chat.completions.create( + model=model, + messages=MESSAGES_ASKING_FOR_TOOLS, + temperature=0, + max_tokens=128, + tool_choice=WEATHER_FORECAST_TOOL_CHOICE, + tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], + logprobs=False, + stream=True, + ) + + async for chunk in stream: + assert chunk.choices[0].index == 0 + + if chunk.choices[0].finish_reason: + finish_reason_count += 1 + assert chunk.choices[0].finish_reason != "tool_calls" + + # if a role is being streamed make sure it wasn't already set to + # something else + if chunk.choices[0].delta.role: + assert not role_name or role_name == "assistant" + role_name = "assistant" + + # if a tool call is streamed make sure there's exactly one + # (based on the request parameters + streamed_tool_calls = chunk.choices[0].delta.tool_calls + + if streamed_tool_calls and len(streamed_tool_calls) > 0: + assert len(streamed_tool_calls) == 1 + tool_call = streamed_tool_calls[0] + + # if a tool call ID is streamed, make sure one hasn't been already + if tool_call.id: + assert not tool_call_id + tool_call_id = tool_call.id + + # if parts of the function start being streamed + if tool_call.function: + # if the function name is defined, set it. it should be streamed + # IN ENTIRETY, exactly one time. + if tool_call.function.name: + assert isinstance(tool_call.function.name, str) + function_name = tool_call.function.name + if tool_call.function.arguments: + assert isinstance(tool_call.function.arguments, str) + function_args_str += tool_call.function.arguments + + assert finish_reason_count == 1 + assert role_name == "assistant" + + # validate the name and arguments + assert function_name == WEATHER_FORECAST_TOOL["function"]["name"] + assert function_name == tool_calls[0].function.name + assert isinstance(function_args_str, str) + + # validate arguments + streamed_args = json.loads(function_args_str) + self.validate_weather_forcast_tool_arguments(streamed_args) + + # make sure everything matches non-streaming except for ID + assert function_name == tool_calls[0].function.name + assert choice.message.role == role_name + assert choice.message.tool_calls[0].function.name == function_name + + @pytest.mark.skipif( + os.environ.get("IMAGE_KIND") == "TRTLLM", + reason="latest release version of Tensorrt LLM 0.18 doesn't support guided decoding", + ) + @pytest.mark.asyncio + async def test_tool_call_with_required_tool_choice( + self, client: openai.AsyncOpenAI, model: str + ): + chat_completion = await client.chat.completions.create( + messages=MESSAGES_ASKING_FOR_TOOLS, + temperature=0, + max_tokens=128, + model=model, + tool_choice="required", + tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], + logprobs=False, + ) + + choice = chat_completion.choices[0] + stop_reason = chat_completion.choices[0].finish_reason + tool_calls = chat_completion.choices[0].message.tool_calls + + # make sure a tool call is present + self.validate_tool_calls_present(tool_calls, skip_id=True) + assert stop_reason != "tool_calls" + + # make sure the weather tool was called (classic example) with arguments + assert tool_calls[0].function.name == WEATHER_TOOL["function"]["name"] + assert tool_calls[0].function.arguments is not None + assert isinstance(tool_calls[0].function.arguments, str) + + # make sure the arguments parse properly + parsed_arguments = json.loads(tool_calls[0].function.arguments) + self.validate_weather_tool_arguments(parsed_arguments) + + function_name: Optional[str] = None + function_args_str: str = "" + tool_call_id: Optional[str] = None + role_name: Optional[str] = None + finish_reason_count: int = 0 + + # make the same request, streaming + stream = await client.chat.completions.create( + model=model, + messages=MESSAGES_ASKING_FOR_TOOLS, + temperature=0, + max_tokens=128, + tool_choice="required", + tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], + logprobs=False, + stream=True, + ) + + async for chunk in stream: + assert chunk.choices[0].index == 0 + + if chunk.choices[0].finish_reason: + finish_reason_count += 1 + assert chunk.choices[0].finish_reason != "tool_calls" + + # if a role is being streamed make sure it wasn't already set to + # something else + if chunk.choices[0].delta.role: + assert not role_name or role_name == "assistant" + role_name = "assistant" + + # if a tool call is streamed make sure there's exactly one + # (based on the request parameters + streamed_tool_calls = chunk.choices[0].delta.tool_calls + + if streamed_tool_calls and len(streamed_tool_calls) > 0: + assert len(streamed_tool_calls) == 1 + tool_call = streamed_tool_calls[0] + + # if a tool call ID is streamed, make sure one hasn't been already + if tool_call.id: + assert not tool_call_id + tool_call_id = tool_call.id + + # if parts of the function start being streamed + if tool_call.function: + # if the function name is defined, set it. it should be streamed + # IN ENTIRETY, exactly one time. + if tool_call.function.name: + assert isinstance(tool_call.function.name, str) + function_name = tool_call.function.name + if tool_call.function.arguments: + assert isinstance(tool_call.function.arguments, str) + function_args_str += tool_call.function.arguments + + assert finish_reason_count == 1 + assert role_name == "assistant" + + # validate the name and arguments + assert function_name == WEATHER_TOOL["function"]["name"] + assert function_name == tool_calls[0].function.name + assert isinstance(function_args_str, str) + + # validate arguments + streamed_args = json.loads(function_args_str) + self.validate_weather_tool_arguments(streamed_args) + + # make sure everything matches non-streaming except for ID + assert function_name == tool_calls[0].function.name + assert choice.message.role == role_name + assert choice.message.tool_calls[0].function.name == function_name + + @pytest.mark.asyncio + async def test_inconsistent_tool_choice_and_tools( + self, client: openai.AsyncOpenAI, model: str + ): + # tool choice function but the tools are empty + with pytest.raises(openai.BadRequestError): + await client.chat.completions.create( + messages=MESSAGES_ASKING_FOR_TOOLS, + temperature=0, + max_tokens=128, + model=model, + tool_choice=WEATHER_FORECAST_TOOL_CHOICE, + logprobs=False, + ) + # tool choice function that is not provided in the tools + with pytest.raises(openai.BadRequestError): + await client.chat.completions.create( + messages=MESSAGES_ASKING_FOR_TOOLS, + temperature=0, + max_tokens=128, + model=model, + tool_choice=WEATHER_FORECAST_TOOL_CHOICE, + tools=[WEATHER_TOOL], + logprobs=False, + ) + + # tool choice required but tools is empty + with pytest.raises(openai.BadRequestError): + await client.chat.completions.create( + messages=MESSAGES_ASKING_FOR_TOOLS, + temperature=0, + max_tokens=128, + model=model, + tool_choice="required", + tools=[], + logprobs=False, + ) diff --git a/python/openai/tests/utils.py b/python/openai/tests/utils.py index 3ed46f70b4..59990ce2a1 100644 --- a/python/openai/tests/utils.py +++ b/python/openai/tests/utils.py @@ -63,7 +63,7 @@ def setup_fastapi_app(tokenizer: str, server: tritonserver.Server, backend: str) # Heavily inspired by vLLM's test infrastructure class OpenAIServer: API_KEY = "EMPTY" # Triton's OpenAI server does not need API key - START_TIMEOUT = 180 # wait for server to start in seconds + START_TIMEOUT = 240 # wait for server to start for up to 240 seconds, mistral model takes longer time to start def __init__( self, diff --git a/python/openai/tests/vllm_mistral_models/mistral-nemo-instruct-2407/1/model.json b/python/openai/tests/vllm_mistral_models/mistral-nemo-instruct-2407/1/model.json new file mode 100644 index 0000000000..835e537349 --- /dev/null +++ b/python/openai/tests/vllm_mistral_models/mistral-nemo-instruct-2407/1/model.json @@ -0,0 +1 @@ +{"model": "mistralai/Mistral-Nemo-Instruct-2407", "disable_log_requests": true, "gpu_memory_utilization": 0.9} \ No newline at end of file diff --git a/python/openai/tests/vllm_mistral_models/mistral-nemo-instruct-2407/config.pbtxt b/python/openai/tests/vllm_mistral_models/mistral-nemo-instruct-2407/config.pbtxt new file mode 100644 index 0000000000..39b3c48edb --- /dev/null +++ b/python/openai/tests/vllm_mistral_models/mistral-nemo-instruct-2407/config.pbtxt @@ -0,0 +1,28 @@ +# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +backend: "vllm" +instance_group [{kind: KIND_MODEL}] diff --git a/qa/L0_openai/generate_engine.py b/qa/L0_openai/generate_engine.py new file mode 100644 index 0000000000..b71e084b03 --- /dev/null +++ b/qa/L0_openai/generate_engine.py @@ -0,0 +1,56 @@ +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from argparse import ArgumentParser + +from tensorrt_llm import LLM, BuildConfig +from tensorrt_llm.plugin import PluginConfig + + +def generate_model_engine(model: str, engines_path: str): + config = BuildConfig(plugin_config=PluginConfig.from_dict({"_gemm_plugin": "auto"})) + + engine = LLM( + model, + dtype="float16", + max_batch_size=128, + build_config=config, + guided_decoding_backend="xgrammar", + ) + + engine.save(engines_path) + engine.shutdown() + + +if __name__ == "__main__": + parser = ArgumentParser() + parser.add_argument( + "--model", "-m", help="model huggingface id or path to the model" + ) + parser.add_argument("--engine_path", "-e", help="directory of the output engine") + FLAGS = parser.parse_args() + + generate_model_engine(FLAGS.model, FLAGS.engine_path) + print(f"model {FLAGS.model}'s engine has been saved to {FLAGS.engine_path}") diff --git a/qa/L0_openai/test.sh b/qa/L0_openai/test.sh index 0921bce98e..9e098a4140 100755 --- a/qa/L0_openai/test.sh +++ b/qa/L0_openai/test.sh @@ -38,7 +38,8 @@ function install_deps() { pip install -r requirements-test.txt if [ "${IMAGE_KIND}" == "TRTLLM" ]; then - prepare_tensorrtllm + prepare_tensorrtllm meta-llama/Meta-Llama-3.1-8B-Instruct tests/tensorrtllm_models /tmp/engines/llama/3.1-8b-instruct/ + prepare_tensorrtllm mistralai/Mistral-Nemo-Instruct-2407 tests/tensorrtllm_mistral_models /tmp/engines/mistral/nemo-instruct-2407/ else prepare_vllm fi @@ -53,36 +54,22 @@ function prepare_tensorrtllm() { # FIXME: Remove when testing TRT-LLM containers built from source pip install -r requirements.txt - MODEL="meta-llama/Meta-Llama-3.1-8B-Instruct" - MODEL_REPO="tests/tensorrtllm_models" + MODEL="$1" + MODEL_REPO="$2" + ENGINE_PATH="$3" + mkdir -p ${MODEL_REPO} cp /app/all_models/inflight_batcher_llm/* "${MODEL_REPO}" -r # Ensemble model is not needed for the test rm -rf ${MODEL_REPO}/ensemble - # 1. Download model from HF - huggingface-cli download ${MODEL} - - HF_LLAMA_MODEL=`python3 -c "from pathlib import Path; from huggingface_hub import hf_hub_download; print(Path(hf_hub_download('${MODEL}', filename='config.json')).parent)"` - CKPT_PATH=/tmp/ckpt/llama/3.1-8b-instruct/ - ENGINE_PATH=/tmp/engines/llama/3.1-8b-instruct/ - - # 2. Convert weights - python3 /app/examples/llama/convert_checkpoint.py --model_dir ${HF_LLAMA_MODEL} \ - --output_dir ${CKPT_PATH} \ - --dtype float16 + # 1. Generate the model's trt engines + python3 ../generate_engine.py --model "${MODEL}" --engine_path "${ENGINE_PATH}" - # 3. Build engine - # max_batch_size set to 128 to avoid OOM errors - trtllm-build --checkpoint_dir ${CKPT_PATH} \ - --gemm_plugin auto \ - --max_batch_size 128 \ - --output_dir ${ENGINE_PATH} - - # 4. Prepare model repository + # 2. Prepare model repository FILL_TEMPLATE="/app/tools/fill_template.py" - python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/preprocessing/config.pbtxt tokenizer_dir:${HF_LLAMA_MODEL},triton_max_batch_size:64,preprocessing_instance_count:1,max_queue_size:0 - python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/postprocessing/config.pbtxt tokenizer_dir:${HF_LLAMA_MODEL},triton_max_batch_size:64,postprocessing_instance_count:1 + python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/preprocessing/config.pbtxt tokenizer_dir:${ENGINE_PATH},triton_max_batch_size:64,preprocessing_instance_count:1,max_queue_size:0 + python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/postprocessing/config.pbtxt tokenizer_dir:${ENGINE_PATH},triton_max_batch_size:64,postprocessing_instance_count:1 python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/tensorrt_llm_bls/config.pbtxt triton_max_batch_size:64,decoupled_mode:True,bls_instance_count:1,accumulate_tokens:False,logits_datatype:TYPE_FP32 python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/tensorrt_llm/config.pbtxt triton_backend:tensorrtllm,triton_max_batch_size:64,decoupled_mode:True,max_beam_width:1,engine_dir:${ENGINE_PATH},batching_strategy:inflight_fused_batching,max_queue_size:0,max_queue_delay_microseconds:1000,encoder_input_features_data_type:TYPE_FP16,logits_datatype:TYPE_FP32,exclude_input_in_output:True } @@ -111,6 +98,18 @@ function run_test() { fi set -e + if [ "$RET" == "0" ]; then + # rerun the tool calling tests with mistral model to cover the mistral tool call parser + set +e + TEST_TOOL_CALL_PARSER="mistral" TEST_TOKENIZER="mistralai/Mistral-Nemo-Instruct-2407" pytest -s -v --junitxml=test_openai.xml tests/test_tool_calling.py 2>&1 > ${TEST_LOG} + if [ $? -ne 0 ]; then + cat ${TEST_LOG} + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + set -e + fi + # Collect logs for error analysis when needed cp *.xml *.log ../../../ popd From 20b8dfff629dde9aebffb4f4e85406246337afb0 Mon Sep 17 00:00:00 2001 From: Pavithra Vijayakrishnan <160681768+pvijayakrish@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:37:23 -0700 Subject: [PATCH 018/230] ci: Remove unsupported PA tests from server repo and CI (#8158) --- qa/L0_perf_analyzer/nginx.conf | 38 - .../perf_analyzer_profile_export_schema.json | 95 -- qa/L0_perf_analyzer/test.sh | 1164 ----------------- qa/L0_perf_analyzer_capi/test.sh | 320 ----- qa/L0_perf_analyzer_doc_links/mkdocs.yml | 36 - qa/L0_perf_analyzer_doc_links/test.sh | 74 -- qa/L0_perf_analyzer_ground_truth/test.sh | 175 --- qa/L0_perf_analyzer_report/test.sh | 175 --- qa/L0_perf_analyzer_unit_tests/test.sh | 50 - 9 files changed, 2127 deletions(-) delete mode 100644 qa/L0_perf_analyzer/nginx.conf delete mode 100644 qa/L0_perf_analyzer/perf_analyzer_profile_export_schema.json delete mode 100755 qa/L0_perf_analyzer/test.sh delete mode 100755 qa/L0_perf_analyzer_capi/test.sh delete mode 100644 qa/L0_perf_analyzer_doc_links/mkdocs.yml delete mode 100755 qa/L0_perf_analyzer_doc_links/test.sh delete mode 100755 qa/L0_perf_analyzer_ground_truth/test.sh delete mode 100755 qa/L0_perf_analyzer_report/test.sh delete mode 100755 qa/L0_perf_analyzer_unit_tests/test.sh diff --git a/qa/L0_perf_analyzer/nginx.conf b/qa/L0_perf_analyzer/nginx.conf deleted file mode 100644 index 4a7dfcc04a..0000000000 --- a/qa/L0_perf_analyzer/nginx.conf +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -server { - listen 443 ssl; - server_name localhost; - - ssl_certificate /etc/nginx/cert.crt; - ssl_certificate_key /etc/nginx/cert.key; - - location / { - proxy_pass http://localhost:8000; - proxy_http_version 1.1; - } -} diff --git a/qa/L0_perf_analyzer/perf_analyzer_profile_export_schema.json b/qa/L0_perf_analyzer/perf_analyzer_profile_export_schema.json deleted file mode 100644 index d0feacd9b4..0000000000 --- a/qa/L0_perf_analyzer/perf_analyzer_profile_export_schema.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/triton-inference-server/client/blob/main/src/c%2B%2B/perf_analyzer/examples/schema.json", - "title": "Perf Analyzer output data", - "description": "A json file describing the output from a Perf Analyzer run.", - "type": "object", - "required": [ - "experiments", - "version" - ], - "properties": { - "experiments": { - "description": "The array of all experiments run by Perf Analyzer.", - "type": "array", - "required": [ - "experiment", - "requests", - "window_boundaries" - ], - "minItems": 1, - "uniqueItems": true, - "items": { - "type": "object", - "properties": { - "experiment": { - "description": "A single experiment run by Perf Analyzer.", - "type": "object", - "required": [ - "mode", - "value" - ], - "minItems": 1, - "maxItems": 1, - "properties": { - "mode": { - "description": "Operating mode of Perf Analyzer: For example, 'concurrency' or 'request rate'.", - "type": "string" - }, - "value": { - "description": "Concurrency or request rate for the current experiment.", - "type": "integer" - } - } - }, - "requests": { - "description": "The array of requests sent by Perf Analyzer for this experiment.", - "type": "array", - "items": { - "$ref": "#/properties/experiments/items/properties/$defs/request" - } - }, - "$defs": { - "request": { - "description": "Info for a single request.", - "type": "object", - "required": [ - "timestamp", - "response_timestamps" - ], - "properties": { - "timestamp": { - "description": "Time stamp of the request.", - "type": "integer" - }, - "sequence_id": { - "description": "The sequence_id of the request.", - "type": "integer" - }, - "response_timestamps": { - "description": "All associated responses to this request.", - "type": "array", - "items": { - "type": "integer" - } - } - } - } - }, - "window_boundaries": { - "description": "An array of time stamps describing window boundaries.", - "type": "array", - "items": { - "type": "integer" - }, - "uniqueItems": true - } - } - } - }, - "version": { - "description": "The version of Perf Analyzer that generated the report.", - "type": "string" - } - } -} \ No newline at end of file diff --git a/qa/L0_perf_analyzer/test.sh b/qa/L0_perf_analyzer/test.sh deleted file mode 100755 index 49c7e72e48..0000000000 --- a/qa/L0_perf_analyzer/test.sh +++ /dev/null @@ -1,1164 +0,0 @@ -#!/bin/bash -# Copyright 2020-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -REPO_VERSION=${NVIDIA_TRITON_SERVER_VERSION} -if [ "$#" -ge 1 ]; then - REPO_VERSION=$1 -fi -if [ -z "$REPO_VERSION" ]; then - echo -e "Repository version must be specified" - echo -e "\n***\n*** Test Failed\n***" - exit 1 -fi -if [ ! -z "$TEST_REPO_ARCH" ]; then - REPO_VERSION=${REPO_VERSION}_${TEST_REPO_ARCH} -fi - -export CUDA_VISIBLE_DEVICES=0 - -CLIENT_LOG="./perf_analyzer.log" -PERF_ANALYZER=../clients/perf_analyzer - -DATADIR=`pwd`/models -TESTDATADIR=`pwd`/test_data - -INT_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/int_data.json -INT_DIFFSHAPE_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/int_data_diff_shape.json -INT_OPTIONAL_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/int_data_optional.json -FLOAT_DIFFSHAPE_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/float_data_with_shape.json -STRING_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/string_data.json -STRING_WITHSHAPE_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/string_data_with_shape.json -SEQ_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/seq_data.json -SHAPETENSORADTAFILE=`pwd`/../common/perf_analyzer_input_data_json/shape_tensor_data.json -IMAGE_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/image_data.json - -OUTPUT_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/output.json -NON_ALIGNED_OUTPUT_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/non_aligned_output.json -WRONG_OUTPUT_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/wrong_output.json -WRONG_OUTPUT_2_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/wrong_output_2.json - -SEQ_OUTPUT_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/seq_output.json -SEQ_WRONG_OUTPUT_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/seq_wrong_output.json - -REPEAT_INT32_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/repeat_int32_data.json - -TRACE_FILE="trace.json" - -SERVER=/opt/tritonserver/bin/tritonserver -SERVER_ARGS="--model-repository=${DATADIR} --trace-config triton,file=${TRACE_FILE}" -SERVER_LOG="./inference_server.log" - -ERROR_STRING="error | Request count: 0 | : 0 infer/sec" - -STABILITY_THRESHOLD="100" - -source ../common/util.sh - -rm -f $SERVER_LOG $CLIENT_LOG -rm -rf $DATADIR $TESTDATADIR $ENSEMBLE_DATADIR - -mkdir -p $DATADIR -# Copy fixed-shape models -cp -r /data/inferenceserver/${REPO_VERSION}/qa_model_repository/graphdef_int32_int32_int32 $DATADIR/ -cp -r /data/inferenceserver/${REPO_VERSION}/qa_model_repository/graphdef_nobatch_int32_int32_int32 $DATADIR/ -cp -r /data/inferenceserver/${REPO_VERSION}/qa_model_repository/graphdef_object_object_object $DATADIR/ -cp -r /data/inferenceserver/${REPO_VERSION}/qa_model_repository/graphdef_nobatch_object_object_object $DATADIR/ - -# Copy a variable-shape models -cp -r /data/inferenceserver/${REPO_VERSION}/qa_variable_model_repository/graphdef_object_int32_int32 $DATADIR/ -cp -r /data/inferenceserver/${REPO_VERSION}/qa_variable_model_repository/graphdef_int32_int32_float32 $DATADIR/ - -# Copy shape tensor models -cp -r /data/inferenceserver/${REPO_VERSION}/qa_shapetensor_model_repository/plan_zero_1_float32_int32 $DATADIR/ - -# Copying ensemble including a sequential model -cp -r /data/inferenceserver/${REPO_VERSION}/qa_sequence_model_repository/savedmodel_sequence_object $DATADIR -cp -r /data/inferenceserver/${REPO_VERSION}/qa_ensemble_model_repository/qa_sequence_model_repository/simple_savedmodel_sequence_object $DATADIR -cp -r /data/inferenceserver/${REPO_VERSION}/qa_ensemble_model_repository/qa_sequence_model_repository/nop_TYPE_FP32_-1 $DATADIR - -# Copying variable sequence model -cp -r /data/inferenceserver/${REPO_VERSION}/qa_variable_sequence_model_repository/graphdef_sequence_float32 $DATADIR - -mkdir $DATADIR/nop_TYPE_FP32_-1/1 - -# Copy inception model to the model repository -cp -r /data/inferenceserver/${REPO_VERSION}/tf_model_store/inception_v1_graphdef $DATADIR - -# Copy resnet50v1.5_fp16 -cp -r /data/inferenceserver/${REPO_VERSION}/perf_model_store/resnet50v1.5_fp16_savedmodel $DATADIR - -# Copy and customize custom_zero_1_float32 -cp -r ../custom_models/custom_zero_1_float32 $DATADIR && \ - mkdir $DATADIR/custom_zero_1_float32/1 && \ - (cd $DATADIR/custom_zero_1_float32 && \ - echo "parameters [" >> config.pbtxt && \ - echo "{ key: \"execute_delay_ms\"; value: { string_value: \"100\" }}" >> config.pbtxt && \ - echo "]" >> config.pbtxt) - -# Copy and customize optional inputs model -cp -r ../python_models/optional $DATADIR && \ - mkdir $DATADIR/optional/1 && \ - mv $DATADIR/optional/model.py $DATADIR/optional/1 && \ - sed -i 's/max_batch_size: 0/max_batch_size: 2/g' $DATADIR/optional/config.pbtxt - -# Copy decoupled model -git clone --depth=1 https://github.com/triton-inference-server/python_backend -mkdir -p $DATADIR/repeat_int32/1 -cp python_backend/examples/decoupled/repeat_config.pbtxt $DATADIR/repeat_int32/config.pbtxt -cp python_backend/examples/decoupled/repeat_model.py $DATADIR/repeat_int32/1/model.py - -# Generating test data -mkdir -p $TESTDATADIR -for INPUT in INPUT0 INPUT1; do - for i in {1..16}; do - echo '1' >> $TESTDATADIR/${INPUT} - done -done - -RET=0 - -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 -fi - - -# Test whether there was a conflict in sending sequences. This should -# be done before other testing as the server might emit this warning -# in certain test cases that are expected to raise this warning -SERVER_ERROR_STRING="The previous sequence did not end before this sequence start" - -set +e -$PERF_ANALYZER -v -i $PROTOCOL -m graphdef_object_object_object -p2000 -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 -if [ $? -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed: Expected an error when using dynamic shapes in string inputs\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "input INPUT0 contains dynamic shape, provide shapes to send along with the request" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed: \n***" - RET=1 -fi - -# Testing with ensemble and sequential model variants -$PERF_ANALYZER -v -i grpc -m simple_savedmodel_sequence_object -p 2000 -t5 --streaming \ ---input-data=$SEQ_JSONDATAFILE --input-data=$SEQ_JSONDATAFILE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed: Sequence conflict when maintaining concurrency\n***" - RET=1 -fi - -$PERF_ANALYZER -v -i grpc -m simple_savedmodel_sequence_object -p 1000 --request-rate-range 100:200:50 --streaming \ ---input-data=$SEQ_JSONDATAFILE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -if [ $(cat $SERVER_LOG | grep "${SERVER_ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $SERVER_LOG | grep "${SERVER_ERROR_STRING}" - echo -e "\n***\n*** Test Failed: Sequence conflict\n***" - RET=1 -fi -set -e - -for PROTOCOL in grpc http; do - - # Testing simple configurations with different shared memory types - for SHARED_MEMORY_TYPE in none system cuda; do - set +e - $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_int32_int32_int32 -t 1 -p2000 -b 1 \ - --shared-memory=$SHARED_MEMORY_TYPE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - - $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_int32_int32_int32 -t 1 -p2000 -b 1 -a \ - --shared-memory=$SHARED_MEMORY_TYPE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - set -e - done - - # TODO Add back testing with preprocess_inception_ensemble model - - # Testing with inception model - for SHARED_MEMORY_TYPE in none system cuda; do - set +e - $PERF_ANALYZER -v -i $PROTOCOL -m inception_v1_graphdef -t 1 -p2000 -b 1 \ - --shared-memory=$SHARED_MEMORY_TYPE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - - $PERF_ANALYZER -v -i $PROTOCOL -m inception_v1_graphdef -t 1 -p2000 -b 1 -a \ - --shared-memory=$SHARED_MEMORY_TYPE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - set -e - done - - # Testing with resnet50 models with large batch sizes - for SHARED_MEMORY_TYPE in none system cuda; do - set +e - $PERF_ANALYZER -v -i $PROTOCOL -m inception_v1_graphdef -t 2 -p2000 -b 64 \ - --shared-memory=$SHARED_MEMORY_TYPE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - - $PERF_ANALYZER -v -i $PROTOCOL -m inception_v1_graphdef -t 2 -p2000 -b 64 \ - --shared-memory=$SHARED_MEMORY_TYPE -a -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - set -e - done - - # Test perf client behavior on different model with different batch size - for MODEL in graphdef_nobatch_int32_int32_int32 graphdef_int32_int32_int32; do - # Valid batch size - set +e - $PERF_ANALYZER -v -i $PROTOCOL -m $MODEL -t 1 -p2000 -b 1 -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - set -e - - # Invalid batch sizes - for STATIC_BATCH in 0 10; do - set +e - $PERF_ANALYZER -v -i $PROTOCOL -m $MODEL -t 1 -p2000 -b $STATIC_BATCH -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - set -e - done - done - - # Testing with the new arguments - set +e - $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_int32_int32_int32 -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - - $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_int32_int32_int32 --concurrency-range 1:5:2 -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "error | Request count: 0 | : 0 infer/sec\|: 0 usec|Request concurrency: 2" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - - $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_int32_int32_int32 --concurrency-range 1:5:2 \ - --input-data=${INT_JSONDATAFILE} -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "error | Request count: 0 | : 0 infer/sec\|: 0 usec|Request concurrency: 2" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - - $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_int32_int32_int32 --request-rate-range 1000:2000:500 \ - -p1000 -b 1 -a -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - - $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_int32_int32_int32 --request-rate-range 1000:2000:500 \ - --input-data=${INT_JSONDATAFILE} -p1000 -b 1 -a -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - - # Binary search for request rate mode - $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_int32_int32_int32 --request-rate-range 1000:2000:100 -p1000 -b 1 \ - -a --binary-search --request-distribution "poisson" -l 10 -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - set -e - - # Binary search for concurrency range mode and make sure it doesn't hang - $PERF_ANALYZER -v -a --request-distribution "poisson" --shared-memory none \ - --percentile 99 --binary-search --concurrency-range 1:8:2 -l 5 \ - -m graphdef_int32_int32_int32 -b 1 -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 & - PA_PID=$! - if [ "$PA_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $PERF_ANALYZER\n***" - cat $CLIENT_LOG - RET=1 - fi - # wait for PA to finish running - sleep 200 - if ps -p $PA_PID > /dev/null; then - cat $CLIENT_LOG - echo -e "\n***\n*** $PERF_ANALYZER is hanging after 200 s\n***" - kill $PA_PID - RET=1 - fi - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - set -e - - # Testing with combinations of string input and shared memory types - for SHARED_MEMORY_TYPE in none system cuda; do - set +e - $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_object_object_object --string-data=1 -p2000 \ - --shared-memory=$SHARED_MEMORY_TYPE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - set -e - done - - # Testing with combinations of file inputs and shared memory types - for SHARED_MEMORY_TYPE in none system cuda; do - set +e - $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_object_object_object --input-data=$TESTDATADIR -p2000 \ - --shared-memory=$SHARED_MEMORY_TYPE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - set -e - done - - for SHARED_MEMORY_TYPE in none system cuda; do - set +e - $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_object_object_object --input-data=$STRING_JSONDATAFILE \ - --input-data=$STRING_JSONDATAFILE -p2000 --shared-memory=$SHARED_MEMORY_TYPE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - set -e - done - - # Testing with combinations of variable inputs and shared memory types - for SHARED_MEMORY_TYPE in none system cuda; do - set +e - $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_object_int32_int32 --input-data=$TESTDATADIR \ - --shape INPUT0:2,8 --shape INPUT1:2,8 -p2000 --shared-memory=$SHARED_MEMORY_TYPE -s ${STABILITY_THRESHOLD} \ - >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - set -e - done - - for SHARED_MEMORY_TYPE in none system cuda; do - set +e - $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_object_int32_int32 --input-data=$STRING_WITHSHAPE_JSONDATAFILE \ - --shape INPUT0:2,8 --shape INPUT1:2,8 -p2000 --shared-memory=$SHARED_MEMORY_TYPE -s ${STABILITY_THRESHOLD} \ - >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - set -e - done - - set +e - $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_int32_int32_float32 --shape INPUT0:2,8,2 \ - --shape INPUT1:2,8,2 -p2000 -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - set -e - - # Trying to batch tensors with different shape - for SHARED_MEMORY_TYPE in none system cuda; do - set +e - $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_int32_int32_float32 --shape INPUT0:2,8,2 --shape INPUT1:2,8,2 -p2000 -b 4 \ - --shared-memory=$SHARED_MEMORY_TYPE --input-data=$INT_DIFFSHAPE_JSONDATAFILE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep -P "The supplied shape .+ is incompatible with the model's input shape" | wc -l) -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - set -e - done - - # Shape tensor I/O model (server needs the shape tensor on the CPU) - for SHARED_MEMORY_TYPE in none system; do - set +e - $PERF_ANALYZER -v -i $PROTOCOL -m plan_zero_1_float32_int32 --input-data=$SHAPETENSORADTAFILE \ - --shape DUMMY_INPUT0:4,4 -p2000 --shared-memory=$SHARED_MEMORY_TYPE -b 8 -s ${STABILITY_THRESHOLD} \ - >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep ": 0 infer/sec\|: 0 usec" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - set -e - done - - set +e - $PERF_ANALYZER -v -i $PROTOCOL -m simple_savedmodel_sequence_object -p 2000 -t5 --sync \ - --input-data=$SEQ_JSONDATAFILE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - - $PERF_ANALYZER -v -i $PROTOCOL -m simple_savedmodel_sequence_object -p 2000 -t5 --sync \ - --input-data=$SEQ_JSONDATAFILE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - - $PERF_ANALYZER -v -i $PROTOCOL -m simple_savedmodel_sequence_object -p 1000 --request-rate-range 100:200:50 --sync \ - --input-data=$SEQ_JSONDATAFILE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - set -e - - - # Testing with variable ensemble model. This unit specifies different shape values - # for different inferences. - for SHARED_MEMORY_TYPE in none system cuda; do - set +e - # FIXME: Enable HTTP when the server is able to correctly return the complex error messages. - $PERF_ANALYZER -v -i grpc -m graphdef_sequence_float32 --shape INPUT:2 --input-data=$FLOAT_DIFFSHAPE_JSONDATAFILE \ - --input-data=$FLOAT_DIFFSHAPE_JSONDATAFILE -p2000 --shared-memory=$SHARED_MEMORY_TYPE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep -P "The supplied shape .+ is incompatible with the model's input shape" | wc -l) -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - set -e - done - - # Testing that trace logging works - set +e - rm ${TRACE_FILE}* - $PERF_ANALYZER -v -i $PROTOCOL -m simple_savedmodel_sequence_object -p 2000 -t5 --sync \ - --trace-level TIMESTAMPS --trace-rate 1000 --trace-count 100 --log-frequency 10 \ - --input-data=$SEQ_JSONDATAFILE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if ! compgen -G "$TRACE_FILE*" > /dev/null; then - echo -e "\n***\n*** Test Failed. $TRACE_FILE failed to generate.\n***" - RET=1 - elif [ $(cat ${TRACE_FILE}* | grep "REQUEST_START" | wc -l) -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed. Did not find `REQUEST_START` in $TRACE_FILE \n***" - RET=1 - fi - curl localhost:8000/v2/trace/setting -d '{"trace_level":["OFF"]}' - set -e - - # Testing that setting trace file does not work - set +e - $PERF_ANALYZER -v -i $PROTOCOL -m simple_savedmodel_sequence_object \ - --trace-file $TRACE_FILE >$CLIENT_LOG 2>&1 - if [ $? -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed. Expected to fail for unknown arg --trace-file" - RET=1 - fi - curl localhost:8000/v2/trace/setting -d '{"trace_level":["OFF"]}' - set -e -done - -# Test with output validation -set +e -$PERF_ANALYZER -v -m graphdef_int32_int32_int32 --input-data=${NON_ALIGNED_OUTPUT_JSONDATAFILE} -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 -if [ $? -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "The 'validation_data' field doesn't align with 'data' field in the json file" | wc -l) -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -$PERF_ANALYZER -v -m graphdef_int32_int32_int32 --input-data=${WRONG_OUTPUT_JSONDATAFILE} -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 -if [ $? -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "mismatch in the data provided" | wc -l) -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -$PERF_ANALYZER -v -m graphdef_int32_int32_int32 --input-data=${WRONG_OUTPUT_2_JSONDATAFILE} -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 -if [ $? -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "Output doesn't match expected output" | wc -l) -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - - -$PERF_ANALYZER -v -m graphdef_int32_int32_int32 --input-data=${OUTPUT_JSONDATAFILE} -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -$PERF_ANALYZER -v -m simple_savedmodel_sequence_object -i grpc --streaming \ ---input-data=${SEQ_WRONG_OUTPUT_JSONDATAFILE} -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 -if [ $? -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "Output doesn't match expected output" | wc -l) -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -$PERF_ANALYZER -v -m simple_savedmodel_sequence_object -i grpc --streaming \ ---input-data=${SEQ_OUTPUT_JSONDATAFILE} -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -set -e - -## Testing with very large concurrencies and large dataset -INPUT_DATA_OPTION="--input-data $SEQ_JSONDATAFILE " -for i in {1..9}; do - INPUT_DATA_OPTION=" ${INPUT_DATA_OPTION} ${INPUT_DATA_OPTION}" -done -set +e -$PERF_ANALYZER -v -m simple_savedmodel_sequence_object -p 10000 --concurrency-range 1500:2000:250 -i grpc --streaming \ -${INPUT_DATA_OPTION} -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -set -e - -## Test count_windows mode -set +e - -# Send incorrect shape and make sure that perf_analyzer doesn't hang -$PERF_ANALYZER -v -m graphdef_object_int32_int32 --measurement-mode "count_windows" \ - --shape INPUT0:1,8,100 --shape INPUT1:2,8 --string-data=1 -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 -if [ $? -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "unexpected shape for input 'INPUT0' for model" | wc -l) -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -$PERF_ANALYZER -v -m graphdef_object_int32_int32 --measurement-mode "count_windows" \ - --shape INPUT0:2,8 --shape INPUT1:2,8 --string-data=1 -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -set -e - -# Test with optional inputs missing but still valid -set +e -$PERF_ANALYZER -v -m optional --measurement-mode "count_windows" \ - --input-data=${INT_OPTIONAL_JSONDATAFILE} -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -set -e - -# Test with optional inputs missing and invalid -set +e -OPTIONAL_INPUT_ERROR_STRING="For batch sizes larger than 1, the same set of -inputs must be specified for each batch. You cannot use different set of -optional inputs for each individual batch." -$PERF_ANALYZER -v -m optional -b 2 --measurement-mode "count_windows" \ - --input-data=${INT_OPTIONAL_JSONDATAFILE} -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 -if [ $? -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "${OPTIONAL_INPUT_ERROR_STRING}" | wc -l) -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -set -e - - -# Test Custom request rate option -CUSTOM_SCHEDULE_FILE=$TESTDATADIR/custom.schedule -echo '30000' >> $CUSTOM_SCHEDULE_FILE -echo '10000' >> $CUSTOM_SCHEDULE_FILE -echo '40000' >> $CUSTOM_SCHEDULE_FILE -echo '20000' >> $CUSTOM_SCHEDULE_FILE -echo '25000' >> $CUSTOM_SCHEDULE_FILE - -set +e -$PERF_ANALYZER -v -i grpc -m graphdef_int32_int32_int32 --request-intervals $CUSTOM_SCHEDULE_FILE >$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "Request Rate: 40" | wc -l) -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed: \n***" - RET=1 -fi -set -e - -# Test --serial-sequences mode -set +e -$PERF_ANALYZER -v -i $PROTOCOL -m simple_savedmodel_sequence_object -p 1000 --request-rate-range 100:200:50 --serial-sequences \ - --input-data=$SEQ_JSONDATAFILE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -$PERF_ANALYZER -v -i $PROTOCOL -m simple_savedmodel_sequence_object -p 1000 --request-intervals $CUSTOM_SCHEDULE_FILE --serial-sequences \ - --input-data=$SEQ_JSONDATAFILE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -set -e - -## Test decoupled model support -$PERF_ANALYZER -v -m repeat_int32 --input-data=$REPEAT_INT32_JSONDATAFILE \ - --profile-export-file profile_export.json -i grpc --async --streaming -s \ - ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -python3 -c "import json ; \ - requests = json.load(open('profile_export.json'))['experiments'][0]['requests'] ; \ - assert any(len(r['response_timestamps']) > 1 for r in requests)" -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -check-jsonschema --schemafile perf_analyzer_profile_export_schema.json profile_export.json -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -## Test perf_analyzer with MPI / multiple models - -is_synchronized() { - local TIMESTAMP_RANK_0_STABLE=$(grep -oP "^\K[^$]+(?=\[1,0\]:All models on all MPI ranks are stable)" 1/rank.0/stdout | date "+%s" -f -) - local TIMESTAMP_RANK_1_STABLE=$(grep -oP "^\K[^$]+(?=\[1,1\]:All models on all MPI ranks are stable)" 1/rank.1/stdout | date "+%s" -f -) - local TIMESTAMP_RANK_2_STABLE=$(grep -oP "^\K[^$]+(?=\[1,2\]:All models on all MPI ranks are stable)" 1/rank.2/stdout | date "+%s" -f -) - local TIMESTAMP_MIN=$(echo -e "${TIMESTAMP_RANK_0_STABLE}\n${TIMESTAMP_RANK_1_STABLE}\n${TIMESTAMP_RANK_2_STABLE}" | sort -n | head -1) - local TIMESTAMP_MAX=$(echo -e "${TIMESTAMP_RANK_0_STABLE}\n${TIMESTAMP_RANK_1_STABLE}\n${TIMESTAMP_RANK_2_STABLE}" | sort -n | tail -1) - local TIMESTAMP_MAX_MIN_DIFFERENCE=$((${TIMESTAMP_MAX}-${TIMESTAMP_MIN})) - local ALLOWABLE_SECONDS_BETWEEN_PROFILES_FINISHING="5" - echo $(($TIMESTAMP_MAX_MIN_DIFFERENCE <= $ALLOWABLE_SECONDS_BETWEEN_PROFILES_FINISHING)) -} - -is_stable() { - local RANK=$1 - local IS_THROUGHPUT=$2 - if [ $IS_THROUGHPUT ]; then - local GREP_PATTERN="\[1,$RANK\]: Pass \[[0-9]+\] throughput: \K[0-9]+\.?[0-9]*" - else - local GREP_PATTERN="\[1,$RANK\]: Pass \[[0-9]+\] throughput: [0-9]+\.?[0-9]* infer/sec. Avg latency: \K[0-9]+" - fi - local LAST_MINUS_0=$(grep -oP "$GREP_PATTERN" 1/rank.$RANK/stdout | tail -3 | sed -n 3p) - local LAST_MINUS_1=$(grep -oP "$GREP_PATTERN" 1/rank.$RANK/stdout | tail -3 | sed -n 2p) - local LAST_MINUS_2=$(grep -oP "$GREP_PATTERN" 1/rank.$RANK/stdout | tail -3 | sed -n 1p) - local MEAN=$(awk "BEGIN {print (($LAST_MINUS_0+$LAST_MINUS_1+$LAST_MINUS_2)/3)}") - local STABILITY_THRESHOLD=0.5 - # Based on this: https://github.com/triton-inference-server/client/blob/main/src/c++/perf_analyzer/inference_profiler.cc#L629-L644 - local WITHIN_THRESHOLD_0=$(awk "BEGIN {print ($LAST_MINUS_0 >= ((1 - $STABILITY_THRESHOLD) * $MEAN) && $LAST_MINUS_0 <= ((1 + $STABILITY_THRESHOLD) * $MEAN))}") - local WITHIN_THRESHOLD_1=$(awk "BEGIN {print ($LAST_MINUS_1 >= ((1 - $STABILITY_THRESHOLD) * $MEAN) && $LAST_MINUS_1 <= ((1 + $STABILITY_THRESHOLD) * $MEAN))}") - local WITHIN_THRESHOLD_2=$(awk "BEGIN {print ($LAST_MINUS_2 >= ((1 - $STABILITY_THRESHOLD) * $MEAN) && $LAST_MINUS_2 <= ((1 + $STABILITY_THRESHOLD) * $MEAN))}") - echo $(($WITHIN_THRESHOLD_0 && $WITHIN_THRESHOLD_1 && $WITHIN_THRESHOLD_2)) -} - -set +e -mpiexec --allow-run-as-root \ - -n 1 --merge-stderr-to-stdout --output-filename . --tag-output --timestamp-output \ - $PERF_ANALYZER -v -m graphdef_int32_int32_int32 \ - --measurement-mode count_windows -s 50 --enable-mpi : \ - -n 1 --merge-stderr-to-stdout --output-filename . --tag-output --timestamp-output \ - $PERF_ANALYZER -v -m graphdef_nobatch_int32_int32_int32 \ - --measurement-mode count_windows -s 50 --enable-mpi : \ - -n 1 --merge-stderr-to-stdout --output-filename . --tag-output --timestamp-output \ - $PERF_ANALYZER -v -m custom_zero_1_float32 \ - --measurement-mode count_windows -s 50 --enable-mpi -if [ $? -ne 0 ]; then - cat 1/rank.0/stdout 1/rank.2/stdout 1/rank.2/stdout - echo -e "\n***\n*** Perf Analyzer returned non-zero exit code\n***" - echo -e "\n***\n*** Test Failed\n***" - RET=1 -else - if [ $(is_synchronized) -eq 0 ]; then - cat 1/rank.0/stdout 1/rank.2/stdout 1/rank.2/stdout - echo -e "\n***\n*** All models did not finish profiling at almost the same time\n***" - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - - RANK_0_THROUGHPUT_IS_STABLE=$(is_stable 0 1) - RANK_0_LATENCY_IS_STABLE=$(is_stable 0 0) - RANK_1_THROUGHPUT_IS_STABLE=$(is_stable 1 1) - RANK_1_LATENCY_IS_STABLE=$(is_stable 1 0) - RANK_2_THROUGHPUT_IS_STABLE=$(is_stable 2 1) - RANK_2_LATENCY_IS_STABLE=$(is_stable 2 0) - - ALL_STABLE=$(( \ - $RANK_0_THROUGHPUT_IS_STABLE && \ - $RANK_0_LATENCY_IS_STABLE && \ - $RANK_1_THROUGHPUT_IS_STABLE && \ - $RANK_1_LATENCY_IS_STABLE && \ - $RANK_2_THROUGHPUT_IS_STABLE && \ - $RANK_2_LATENCY_IS_STABLE)) - - if [ $ALL_STABLE -eq 0 ]; then - cat 1/rank.0/stdout 1/rank.2/stdout 1/rank.2/stdout - echo -e "\n***\n*** All models did not stabilize\n***" - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - - rm -rf 1 -fi -set -e - -## Test perf_analyzer without MPI library (`libmpi.so`) available - -rm -rf /opt/hpcx/ompi/lib/libmpi* - -set +e -$PERF_ANALYZER -v -m graphdef_int32_int32_int32 -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -set -e - -kill $SERVER_PID -wait $SERVER_PID - -# Generate valid CA -openssl genrsa -passout pass:1234 -des3 -out ca.key 4096 -openssl req -passin pass:1234 -new -x509 -days 365 -key ca.key -out ca.crt -subj "/C=SP/ST=Spain/L=Valdepenias/O=Test/OU=Test/CN=Root CA" - -# Generate valid Server Key/Cert -openssl genrsa -passout pass:1234 -des3 -out server.key 4096 -openssl req -passin pass:1234 -new -key server.key -out server.csr -subj "/C=SP/ST=Spain/L=Valdepenias/O=Test/OU=Server/CN=localhost" -openssl x509 -req -passin pass:1234 -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt - -# Remove passphrase from the Server Key -openssl rsa -passin pass:1234 -in server.key -out server.key - -# Generate valid Client Key/Cert -openssl genrsa -passout pass:1234 -des3 -out client.key 4096 -openssl req -passin pass:1234 -new -key client.key -out client.csr -subj "/C=SP/ST=Spain/L=Valdepenias/O=Test/OU=Client/CN=localhost" -openssl x509 -passin pass:1234 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt - -# Remove passphrase from Client Key -openssl rsa -passin pass:1234 -in client.key -out client.key - -# Create mutated client key (Make first char of each like capital) -cp client.key client2.key && sed -i "s/\b\(.\)/\u\1/g" client2.key -cp client.crt client2.crt && sed -i "s/\b\(.\)/\u\1/g" client2.crt - -SERVER_ARGS="--model-repository=${DATADIR} --grpc-use-ssl=1 --grpc-server-cert=server.crt --grpc-server-key=server.key --grpc-root-cert=ca.crt" - -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 -fi - -# Test gRPC SSL -set +e - -# Test that gRPC protocol with SSL works correctly -$PERF_ANALYZER -v -i grpc -m graphdef_int32_int32_int32 \ - --ssl-grpc-use-ssl \ - --ssl-grpc-root-certifications-file=ca.crt \ - --ssl-grpc-private-key-file=client.key \ - --ssl-grpc-certificate-chain-file=client.crt \ - -s ${STABILITY_THRESHOLD} \ - > ${CLIENT_LOG}.grpc_success 2>&1 -if [ $? -ne 0 ]; then - cat ${CLIENT_LOG}.grpc_success - RET=1 -fi - -# Test that gRPC protocol with SSL fails with incorrect key -$PERF_ANALYZER -v -i grpc -m graphdef_int32_int32_int32 \ - --ssl-grpc-use-ssl \ - --ssl-grpc-root-certifications-file=ca.crt \ - --ssl-grpc-private-key-file=client.key \ - --ssl-grpc-certificate-chain-file=client2.crt \ - -s ${STABILITY_THRESHOLD} \ - > ${CLIENT_LOG}.grpc_failure 2>&1 -if [ $? -eq 0 ]; then - cat ${CLIENT_LOG}.grpc_failure - echo -e "\n***\n*** Expected test failure\n***" - RET=1 -fi - -set -e - -kill $SERVER_PID -wait $SERVER_PID - -cp server.crt /etc/nginx/cert.crt -cp server.key /etc/nginx/cert.key - -SERVER_ARGS="--model-repository=${DATADIR}" - -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 -fi - -# Setup the new configuration for the proxy. The HTTPS traffic will be -# redirected to the running instance of server at localhost:8000 -cp nginx.conf /etc/nginx/sites-available/default - -# Start the proxy server -service nginx restart - -# Test HTTP SSL -set +e - -# Test that HTTP protocol with SSL works correctly with certificates -$PERF_ANALYZER -v -u https://localhost:443 -i http -m graphdef_int32_int32_int32 \ - --ssl-https-verify-peer 1 \ - --ssl-https-verify-host 2 \ - --ssl-https-ca-certificates-file ca.crt \ - --ssl-https-client-certificate-file client.crt \ - --ssl-https-client-certificate-type PEM \ - --ssl-https-private-key-file client.key \ - --ssl-https-private-key-type PEM \ - -s ${STABILITY_THRESHOLD} \ - > ${CLIENT_LOG}.https_success 2>&1 -if [ $? -ne 0 ]; then - cat ${CLIENT_LOG}.https_success - RET=1 -fi - -# Test that HTTP protocol with SSL works correctly without certificates -$PERF_ANALYZER -v -u https://localhost:443 -i http -m graphdef_int32_int32_int32 \ - --ssl-https-verify-peer 0 \ - --ssl-https-verify-host 0 \ - -s ${STABILITY_THRESHOLD} \ - > ${CLIENT_LOG}.https_success 2>&1 -if [ $? -ne 0 ]; then - cat ${CLIENT_LOG}.https_success - RET=1 -fi - -# Test that HTTP protocol with SSL fails with incorrect key -$PERF_ANALYZER -v -u https://localhost:443 -i http -m graphdef_int32_int32_int32 \ - --ssl-https-verify-peer 1 \ - --ssl-https-verify-host 2 \ - --ssl-https-ca-certificates-file ca.crt \ - --ssl-https-client-certificate-file client.crt \ - --ssl-https-client-certificate-type PEM \ - --ssl-https-private-key-file client2.key \ - --ssl-https-private-key-type PEM \ - -s ${STABILITY_THRESHOLD} \ - > ${CLIENT_LOG}.https_failure 2>&1 -if [ $? -eq 0 ]; then - cat ${CLIENT_LOG}.https_failure - echo -e "\n***\n*** Expected test failure\n***" - RET=1 -fi - -set -e - -kill $SERVER_PID -wait $SERVER_PID - -if [ $RET -eq 0 ]; then - echo -e "\n***\n*** Test Passed\n***" -else - echo -e "\n***\n*** Test FAILED\n***" -fi - -exit $RET diff --git a/qa/L0_perf_analyzer_capi/test.sh b/qa/L0_perf_analyzer_capi/test.sh deleted file mode 100755 index 53196fa762..0000000000 --- a/qa/L0_perf_analyzer_capi/test.sh +++ /dev/null @@ -1,320 +0,0 @@ -#!/bin/bash -# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -# TESTS COPIED FROM L0_perf_analyzer/test.sh -REPO_VERSION=${NVIDIA_TRITON_SERVER_VERSION} -if [ "$#" -ge 1 ]; then - REPO_VERSION=$1 -fi -if [ -z "$REPO_VERSION" ]; then - echo -e "Repository version must be specified" - echo -e "\n***\n*** Test Failed\n***" - exit 1 -fi -if [ ! -z "$TEST_REPO_ARCH" ]; then - REPO_VERSION=${REPO_VERSION}_${TEST_REPO_ARCH} -fi - -export CUDA_VISIBLE_DEVICES=0 - -CLIENT_LOG="./perf_analyzer.log" -PERF_ANALYZER=../clients/perf_analyzer - -DATADIR=`pwd`/models -TESTDATADIR=`pwd`/test_data - -SERVER_LIBRARY_PATH=/opt/tritonserver - -FLOAT_DIFFSHAPE_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/float_data_with_shape.json -STRING_WITHSHAPE_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/string_data_with_shape.json -SEQ_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/seq_data.json -SHAPETENSORADTAFILE=`pwd`/../common/perf_analyzer_input_data_json/shape_tensor_data.json - -ERROR_STRING="error | Request count: 0 | : 0 infer/sec" - -STABILITY_THRESHOLD="9999" - -source ../common/util.sh - -rm -f $CLIENT_LOG -rm -rf $DATADIR $TESTDATADIR $ENSEMBLE_DATADIR - -mkdir -p $DATADIR -# Copy fixed-shape models -cp -r /data/inferenceserver/${REPO_VERSION}/qa_model_repository/graphdef_int32_int32_int32 $DATADIR/ -cp -r /data/inferenceserver/${REPO_VERSION}/qa_model_repository/graphdef_object_object_object $DATADIR/ - -# Copy a variable-shape models -cp -r /data/inferenceserver/${REPO_VERSION}/qa_variable_model_repository/graphdef_object_int32_int32 $DATADIR/ -cp -r /data/inferenceserver/${REPO_VERSION}/qa_variable_model_repository/graphdef_int32_int32_float32 $DATADIR/ - -# Copy shape tensor models -cp -r /data/inferenceserver/${REPO_VERSION}/qa_shapetensor_model_repository/plan_zero_1_float32_int32 $DATADIR/ - -# Copying ensemble including a sequential model -cp -r /data/inferenceserver/${REPO_VERSION}/qa_sequence_model_repository/savedmodel_sequence_object $DATADIR -cp -r /data/inferenceserver/${REPO_VERSION}/qa_ensemble_model_repository/qa_sequence_model_repository/simple_savedmodel_sequence_object $DATADIR - -# Copying variable sequence model -cp -r /data/inferenceserver/${REPO_VERSION}/qa_variable_sequence_model_repository/graphdef_sequence_float32 $DATADIR - -# Copying bls model with undefined variable -mkdir -p $DATADIR/bls_undefined/1 && \ - cp ../python_models/bls_undefined/model.py $DATADIR/bls_undefined/1/. && \ - cp ../python_models/bls_undefined/config.pbtxt $DATADIR/bls_undefined/. - -# Generating test data -mkdir -p $TESTDATADIR -for INPUT in INPUT0 INPUT1; do - for i in {1..16}; do - echo '1' >> $TESTDATADIR/${INPUT} - done -done - -RET=0 - -########## Test C API ############# -# Make sure tritonserver is not running first -set +e -SERVER_PID=$(pidof tritonserver) -if [ $? -ne 1 ]; then -echo -e "\n There was a previous instance of tritonserver, killing \n" - kill $SERVER_PID - wait $SERVER_PID -fi -set -e - -# Testing simple configuration -$PERF_ANALYZER -v -m graphdef_int32_int32_int32 \ ---service-kind=triton_c_api \ ---model-repository=$DATADIR --triton-server-directory=$SERVER_LIBRARY_PATH \ --s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -$PERF_ANALYZER -v -m graphdef_int32_int32_int32 -t 1 -p2000 -b 1 \ ---service-kind=triton_c_api --model-repository=$DATADIR \ ---triton-server-directory=$SERVER_LIBRARY_PATH -s ${STABILITY_THRESHOLD} \ ->$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -#Testing with string input -$PERF_ANALYZER -v -m graphdef_object_object_object --string-data=1 -p2000 \ ---service-kind=triton_c_api --model-repository=$DATADIR \ ---triton-server-directory=$SERVER_LIBRARY_PATH -s ${STABILITY_THRESHOLD} \ ->$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -# Testing with variable inputs -$PERF_ANALYZER -v -m graphdef_object_int32_int32 --input-data=$TESTDATADIR \ ---shape INPUT0:2,8 --shape INPUT1:2,8 \ ---service-kind=triton_c_api --model-repository=$DATADIR \ ---triton-server-directory=$SERVER_LIBRARY_PATH -s ${STABILITY_THRESHOLD} \ ->$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -$PERF_ANALYZER -v -m graphdef_object_int32_int32 \ ---input-data=$STRING_WITHSHAPE_JSONDATAFILE \ ---shape INPUT0:2,8 --shape INPUT1:2,8 -p2000 \ ---service-kind=triton_c_api --model-repository=$DATADIR \ ---triton-server-directory=$SERVER_LIBRARY_PATH -s ${STABILITY_THRESHOLD} \ ->$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -$PERF_ANALYZER -v -m graphdef_int32_int32_float32 --shape INPUT0:2,8,2 \ ---shape INPUT1:2,8,2 -p2000 \ ---service-kind=triton_c_api --model-repository=$DATADIR \ ---triton-server-directory=$SERVER_LIBRARY_PATH -s ${STABILITY_THRESHOLD} \ ->$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -# Shape tensor I/O model (server needs the shape tensor on the CPU) -$PERF_ANALYZER -v -m plan_zero_1_float32_int32 --input-data=$SHAPETENSORADTAFILE \ ---shape DUMMY_INPUT0:4,4 -p2000 -b 8 \ ---service-kind=triton_c_api --model-repository=$DATADIR \ ---triton-server-directory=$SERVER_LIBRARY_PATH -s ${STABILITY_THRESHOLD} \ ->$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep ": 0 infer/sec\|: 0 usec" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -$PERF_ANALYZER -v -m simple_savedmodel_sequence_object -p 2000 -t5 --sync \ --s ${STABILITY_THRESHOLD} \ ---input-data=$SEQ_JSONDATAFILE \ ---service-kind=triton_c_api --model-repository=$DATADIR \ ---triton-server-directory=$SERVER_LIBRARY_PATH >$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -set +e -$PERF_ANALYZER -v -m graphdef_sequence_float32 --shape INPUT:2 \ --s ${STABILITY_THRESHOLD} \ ---input-data=$FLOAT_DIFFSHAPE_JSONDATAFILE \ ---input-data=$FLOAT_DIFFSHAPE_JSONDATAFILE -p2000 \ ---service-kind=triton_c_api --model-repository=$DATADIR \ ---triton-server-directory=$SERVER_LIBRARY_PATH --sync >$CLIENT_LOG 2>&1 -if [ $? -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep -P "The supplied shape .+ is incompatible with the model's input shape" | wc -l) -eq 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -set -e - -for SHARED_MEMORY_TYPE in system cuda; do - $PERF_ANALYZER -v -m graphdef_int32_int32_int32 -t 1 -p2000 -b 1 \ - -s ${STABILITY_THRESHOLD} \ - --shared-memory=$SHARED_MEMORY_TYPE \ - --service-kind=triton_c_api --model-repository=$DATADIR \ - --triton-server-directory=$SERVER_LIBRARY_PATH >$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi -done - - -$PERF_ANALYZER -v -m graphdef_int32_int32_int32 --request-rate-range 1000:2000:500 -p1000 -b 1 \ ---service-kind=triton_c_api --model-repository=$DATADIR \ ---triton-server-directory=$SERVER_LIBRARY_PATH -s ${STABILITY_THRESHOLD} \ ->$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -set -e - -set +e -# Testing erroneous configuration -# This model is expected to fail -$PERF_ANALYZER -v -m bls_undefined --shape INPUT0:1048576 -t 64\ ---service-kind=triton_c_api \ ---model-repository=$DATADIR --triton-server-directory=$SERVER_LIBRARY_PATH \ --s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 -if [ $? -ne 99 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -set -e - -# Make sure server is not still running -set +e -SERVER_PID=$(pidof tritonserver) -if [ $? -eq 0 ]; then - echo -e "\n Tritonserver did not exit properly, killing \n" - kill $SERVER_PID - wait $SERVER_PID - RET=1 -fi -set -e - -if [ $RET -eq 0 ]; then - echo -e "\n***\n*** Test Passed\n***" -else - echo -e "\n***\n*** Test FAILED\n***" -fi -exit $RET diff --git a/qa/L0_perf_analyzer_doc_links/mkdocs.yml b/qa/L0_perf_analyzer_doc_links/mkdocs.yml deleted file mode 100644 index 41a4bfe485..0000000000 --- a/qa/L0_perf_analyzer_doc_links/mkdocs.yml +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -site_name: CI Test -use_directory_urls: False -docs_dir: "./docs" -plugins: - - htmlproofer - - search - -markdown_extensions: - - toc: - permalink: True diff --git a/qa/L0_perf_analyzer_doc_links/test.sh b/qa/L0_perf_analyzer_doc_links/test.sh deleted file mode 100755 index d0757bca9e..0000000000 --- a/qa/L0_perf_analyzer_doc_links/test.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/bin/bash -# Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -LOG="`pwd`/doc_links.log" -CONFIG="`pwd`/mkdocs.yml" -RET=0 - -# Download necessary packages -python3 -m pip install mkdocs -python3 -m pip install mkdocs-htmlproofer-plugin==0.10.3 - -#Download perf_analyzer docs -TRITON_REPO_ORGANIZATION=${TRITON_REPO_ORGANIZATION:="http://github.com/triton-inference-server"} -TRITON_PERF_ANALYZER_REPO_TAG="${TRITON_PERF_ANALYZER_REPO_TAG:=main}" -git clone -b ${TRITON_PERF_ANALYZER_REPO_TAG} ${TRITON_REPO_ORGANIZATION}/perf_analyzer.git -cp `pwd`/perf_analyzer/README.md . -cp -rf `pwd`/perf_analyzer/docs . - -# Need to remove all links that start with -- or -. Mkdocs converts all -- to - for anchor links. -# This breaks all links to cli commands throughout the docs. This will iterate over all -# files in the docs directory and remove -- and - at the start of options, which allows the -# tool to check links for correctness. -for file in `pwd`/docs/*.md -do - echo $file - sed -i 's/`-*/`/g' $file - sed -i 's/#-*/#/g' $file -done - -exec mkdocs serve -f $CONFIG > $LOG & -PID=$! -sleep 20 - -until [[ (-z `pgrep mkdocs`) ]]; do - kill -2 $PID - sleep 2 -done - -if [[ ! -z `grep "invalid url" $LOG` ]]; then - cat $LOG - RET=1 -fi - - -if [ $RET -eq 0 ]; then - echo -e "\n***\n*** Test PASSED\n***" -else - echo -e "\n***\n*** Test FAILED\n***" -fi -exit $RET diff --git a/qa/L0_perf_analyzer_ground_truth/test.sh b/qa/L0_perf_analyzer_ground_truth/test.sh deleted file mode 100755 index d5d78e63f4..0000000000 --- a/qa/L0_perf_analyzer_ground_truth/test.sh +++ /dev/null @@ -1,175 +0,0 @@ -#!/bin/bash -# Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -REPO_VERSION=${NVIDIA_TRITON_SERVER_VERSION} -if [ "$#" -ge 1 ]; then - REPO_VERSION=$1 -fi -if [ -z "${REPO_VERSION}" ]; then - echo -e "Repository version must be specified" - echo -e "\n***\n*** Test Failed\n***" - exit 1 -fi -if [ ! -z "$TEST_REPO_ARCH" ]; then - REPO_VERSION=${REPO_VERSION}_${TEST_REPO_ARCH} -fi - -source ../common/util.sh - -# Setup client/perf_analyzer -CLIENT_LOG="./perf_analyzer.log" -PERF_ANALYZER=../clients/perf_analyzer - -function check_perf_analyzer_error { - ERROR_STRING="error | Request count: 0 | : 0 infer/sec" - CLIENT_RET="$1" - if [ ${CLIENT_RET} -ne 0 ]; then - cat ${CLIENT_LOG} - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat ${CLIENT_LOG} | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat ${CLIENT_LOG} - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi -} - -# Checks that the model infer/sec performance is equal to an expected value -# +/- some tolerance. -# $1: csv result file from PA run -# $2: expected infer/sec value -# $3: tolerance for expected value equality -function check_performance { - # get the boundary values based on the tolerance percentage - MIN=$(python3 -c "print(${2} * (1 - ${3}))") - MAX=$(python3 -c "print(${2} * (1 + ${3}))") - - # delete all but the 2nd line in the resulting file - # then get the 2nd column value which is the infer/sec measurement - report_val=$(sed '2!d' $1 | awk -F ',' {'print $2'}) - - # check if within tolerance - ret=$(python3 -c "print(${report_val} >= ${MIN} and ${report_val} <= ${MAX})") - if [ "$ret" = "False" ]; then - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi -} - -# Iterate over the grpc results to ensure gRPC times are greater than 0 -# $1: client log file -# example line: Avg gRPC time: 42648 usec (marshal 6 usec + response wait 42640 usec + unmarshal 2 usec) -function check_grpc_time { - grep "gRPC" $1 | awk '{print $4}' | while read -r line; do - if [ $line -eq 0 ]; then - RET=1 - fi - done -} - -# Create input_data.json to communicate the requested model delay -# $1: desired model delay -function create_input_data { - echo "{\"data\":[{\"INPUT0\" : [${1}]}]}" > input_data.json -} - -# Setup server -export CUDA_VISIBLE_DEVICES=0 -SERVER=/opt/tritonserver/bin/tritonserver -SERVER_ARGS="--model-repository=`pwd`/models" -SERVER_LOG="./inference_server.log" - -rm -f $SERVER_LOG $CLIENT_LOG -MODEL_DIR="./models" -rm -fr ${MODEL_DIR} && mkdir ${MODEL_DIR} -MODELS="ground_truth" - -for model in ${MODELS}; do - # Add version directory to each model if non-existent - mkdir -p "${MODEL_DIR}/${model}/1" - cp ../python_models/${model}/model.py ./models/${model}/1/model.py - cp ../python_models/${model}/config.pbtxt ./models/${model}/config.pbtxt -done - -# Run server -run_server -if [ "${SERVER_PID}" == "0" ]; then - echo -e "\n***\n*** Failed to start ${SERVER}\n***" - cat ${SERVER_LOG} - exit 1 -fi - -# Run perf_analyzer -set +e -RET=0 -PROTOCOLS="http grpc" -OUTPUT_FILE="results" -MODEL_DELAYS=(0.05 0.5) -TOLERANCE="0.05" - -for model_delay in ${MODEL_DELAYS[@]}; do - create_input_data ${model_delay} - EXPECTED_RESULT=$(python3 -c "print(1 / ${model_delay})") - for protocol in ${PROTOCOLS}; do - for model in ${MODELS}; do - echo "================================================================" - echo "[PERMUTATION] Protocol=${protocol} Model=${model}" - echo "================================================================" - - ${PERF_ANALYZER} -v -i ${protocol} --concurrency-range 2 --input-data input_data.json -m ${model} -f ${OUTPUT_FILE} | tee ${CLIENT_LOG} 2>&1 - check_perf_analyzer_error $? - - check_performance ${OUTPUT_FILE} ${EXPECTED_RESULT} ${TOLERANCE} - - if [ "${protocol}" == "grpc" ]; then - check_grpc_time ${CLIENT_LOG} - fi - done; - done; -done; - - -set -e - -# Cleanup -kill $SERVER_PID -wait $SERVER_PID - -if [ $RET -eq 0 ]; then - echo -e "\n***\n*** Test Passed\n***" -else - echo "=== START SERVER LOG ===" - cat ${SERVER_LOG} - echo "=== END SERVER LOG ===" - echo "=== START CLIENT LOG ===" - cat ${CLIENT_LOG} - echo "=== END CLIENT LOG ===" - echo -e "\n***\n*** Test FAILED\n***" -fi - -exit ${RET} diff --git a/qa/L0_perf_analyzer_report/test.sh b/qa/L0_perf_analyzer_report/test.sh deleted file mode 100755 index 469d11ce3a..0000000000 --- a/qa/L0_perf_analyzer_report/test.sh +++ /dev/null @@ -1,175 +0,0 @@ -#!/bin/bash -# Copyright 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -REPO_VERSION=${NVIDIA_TRITON_SERVER_VERSION} -if [ "$#" -ge 1 ]; then - REPO_VERSION=$1 -fi -if [ -z "${REPO_VERSION}" ]; then - echo -e "Repository version must be specified" - echo -e "\n***\n*** Test Failed\n***" - exit 1 -fi -if [ ! -z "$TEST_REPO_ARCH" ]; then - REPO_VERSION=${REPO_VERSION}_${TEST_REPO_ARCH} -fi - -source ../common/util.sh - -# Setup client/perf_analyzer -CLIENT_LOG="./perf_analyzer.log" -PERF_ANALYZER=../clients/perf_analyzer - -function check_perf_analyzer_error { - ERROR_STRING="error | Request count: 0 | : 0 infer/sec" - CLIENT_RET="$1" - if [ ${CLIENT_RET} -ne 0 ]; then - cat ${CLIENT_LOG} - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - if [ $(cat ${CLIENT_LOG} | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then - cat ${CLIENT_LOG} - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi -} - -function check_cache_output { - # Validate cache info in perf_analyzer output - CACHE_STRING="Cache hit count" - if [ $(cat ${CLIENT_LOG} | grep -i "${CACHE_STRING}" | wc -l) -eq 0 ]; then - cat ${CLIENT_LOG} - echo "ERROR: No cache hit count found in output" - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - - # Validate non-zero number of cache hits - ERROR_STRING="Cache hit count: 0" - num_cache_hit_lines=$(cat ${CLIENT_LOG} | grep -i "${CACHE_STRING}" | wc -l) - num_cache_hit_zero_lines=$(cat ${CLIENT_LOG} | grep -i "${ERROR_STRING}" | wc -l) - if [ ${num_cache_hit_zero_lines} -eq ${num_cache_hit_lines} ]; then - cat ${CLIENT_LOG} - echo "ERROR: All cache hit counts were zero, expected a non-zero number of cache hits" - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi -} - -# Setup server -export CUDA_VISIBLE_DEVICES=0 -SERVER=/opt/tritonserver/bin/tritonserver -# --response-cache-byte-size must be non-zero to test models with cache enabled -SERVER_ARGS="--model-repository=`pwd`/models --response-cache-byte-size=8192" -SERVER_LOG="./inference_server.log" - -# Setup model repository from existing qa_model_repository -rm -f $SERVER_LOG $CLIENT_LOG -MODEL_DIR="./models" -rm -fr ${MODEL_DIR} && mkdir ${MODEL_DIR} -ENSEMBLE_MODEL="simple_onnx_float32_float32_float32" -COMPOSING_MODEL="onnx_float32_float32_float32" -ENSEMBLE_MODEL_CACHE_ENABLED="${ENSEMBLE_MODEL}_cache_enabled" -ENSEMBLE_MODEL_CACHE_DISABLED="${ENSEMBLE_MODEL}_cache_disabled" -COMPOSING_MODEL_CACHE_ENABLED="${COMPOSING_MODEL}_cache_enabled" -COMPOSING_MODEL_CACHE_DISABLED="${COMPOSING_MODEL}_cache_disabled" -MODELS="${ENSEMBLE_MODEL_CACHE_ENABLED} ${ENSEMBLE_MODEL_CACHE_DISABLED} ${COMPOSING_MODEL_CACHE_ENABLED} ${COMPOSING_MODEL_CACHE_DISABLED}" - -## Setup ensemble models, one with cache enabled and one with cache disabled -cp -r "/data/inferenceserver/${REPO_VERSION}/qa_ensemble_model_repository/qa_model_repository/${ENSEMBLE_MODEL}" "${MODEL_DIR}/${ENSEMBLE_MODEL_CACHE_ENABLED}" -cp -r "/data/inferenceserver/${REPO_VERSION}/qa_ensemble_model_repository/qa_model_repository/${ENSEMBLE_MODEL}" "${MODEL_DIR}/${ENSEMBLE_MODEL_CACHE_DISABLED}" - -## Setup composing models, one with cache enabled and one with cache disabled -cp -r "/data/inferenceserver/${REPO_VERSION}/qa_model_repository/${COMPOSING_MODEL}" "${MODEL_DIR}/${COMPOSING_MODEL_CACHE_ENABLED}" -cp -r "/data/inferenceserver/${REPO_VERSION}/qa_model_repository/${COMPOSING_MODEL}" "${MODEL_DIR}/${COMPOSING_MODEL_CACHE_DISABLED}" - -for model in ${MODELS}; do - # Remove "name" line from each config to use directory name for simplicity - sed -i "/^name:/d" "${MODEL_DIR}/${model}/config.pbtxt" - # Add version directory to each model if non-existent - mkdir -p "${MODEL_DIR}/${model}/1" -done - -## Update "model_name" lines in each ensemble model config ensemble steps -sed -i "s/${COMPOSING_MODEL}/${COMPOSING_MODEL_CACHE_ENABLED}/g" "${MODEL_DIR}/${ENSEMBLE_MODEL_CACHE_ENABLED}/config.pbtxt" -sed -i "s/${COMPOSING_MODEL}/${COMPOSING_MODEL_CACHE_DISABLED}/g" "${MODEL_DIR}/${ENSEMBLE_MODEL_CACHE_DISABLED}/config.pbtxt" - -## Append cache config to each model config -echo -e "response_cache { enable: True }" >> "${MODEL_DIR}/${ENSEMBLE_MODEL_CACHE_ENABLED}/config.pbtxt" -echo -e "response_cache { enable: False }" >> "${MODEL_DIR}/${ENSEMBLE_MODEL_CACHE_DISABLED}/config.pbtxt" -echo -e "response_cache { enable: True }" >> "${MODEL_DIR}/${COMPOSING_MODEL_CACHE_ENABLED}/config.pbtxt" -echo -e "response_cache { enable: False }" >> "${MODEL_DIR}/${COMPOSING_MODEL_CACHE_DISABLED}/config.pbtxt" -# Force CPU memory for composing models since cache doesn't currently support GPU memory -echo -e "instance_group [{ kind: KIND_CPU, count: 1 }]" >> "${MODEL_DIR}/${COMPOSING_MODEL_CACHE_ENABLED}/config.pbtxt" -echo -e "instance_group [{ kind: KIND_CPU, count: 1 }]" >> "${MODEL_DIR}/${COMPOSING_MODEL_CACHE_DISABLED}/config.pbtxt" - -# Run server -run_server -if [ "${SERVER_PID}" == "0" ]; then - echo -e "\n***\n*** Failed to start ${SERVER}\n***" - cat ${SERVER_LOG} - exit 1 -fi - -# Run perf_analyzer -set +e -RET=0 -PROTOCOLS="http grpc" -STABILITY_THRESHOLD="15" -for protocol in ${PROTOCOLS}; do - for model in ${MODELS}; do - echo "================================================================" - echo "[PERMUTATION] Protocol=${protocol} Model=${model}" - echo "================================================================" - - ${PERF_ANALYZER} -v -i ${protocol} -m ${model} -s ${STABILITY_THRESHOLD} | tee ${CLIENT_LOG} 2>&1 - check_perf_analyzer_error $? - - # Check response cache outputs - if [[ ${model} == *"cache_enabled"* ]]; then - check_cache_output - fi - done; -done; -set -e - -# Cleanup -kill $SERVER_PID -wait $SERVER_PID - - -if [ $RET -eq 0 ]; then - echo -e "\n***\n*** Test Passed\n***" -else - echo "=== START SERVER LOG ===" - cat ${SERVER_LOG} - echo "=== END SERVER LOG ===" - echo -e "\n***\n*** Test FAILED\n***" -fi - -exit ${RET} diff --git a/qa/L0_perf_analyzer_unit_tests/test.sh b/qa/L0_perf_analyzer_unit_tests/test.sh deleted file mode 100755 index f2a70d23ff..0000000000 --- a/qa/L0_perf_analyzer_unit_tests/test.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash -# Copyright 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -TEST_LOG="./perf_analyzer_unit_tests.log" -PERF_ANALYZER_UNIT_TESTS=../clients/perf_analyzer_unit_tests - -RET=0 - -rm -f $TEST_LOG - -set +e -$PERF_ANALYZER_UNIT_TESTS >> $TEST_LOG 2>&1 -if [ $? -ne 0 ]; then - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi -set -e - -if [ $RET -eq 0 ]; then - echo -e "\n***\n*** Test Passed\n***" -else - cat $TEST_LOG - echo -e "\n***\n*** Test FAILED\n***" -fi - -exit $RET From 901b0e9ddc3c0bee2c96558ad28a6865a41c2b3f Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Fri, 18 Apr 2025 19:18:06 -0400 Subject: [PATCH 019/230] test: Add config parameter "execution_context_allocation_strategy" to TensorRT backend (#8150) --- .../expected | 1 + .../partial.pbtxt | 31 +++++++ .../allocation_strategy_no_key/partial.pbtxt | 26 ++++++ .../partial.pbtxt | 25 ++++++ .../allocation_strategy_value_1/expected | 1 + .../allocation_strategy_value_1/partial.pbtxt | 31 +++++++ .../allocation_strategy_value_2/expected | 1 + .../allocation_strategy_value_2/partial.pbtxt | 31 +++++++ qa/L0_model_config/test.sh | 90 ++++++++++++++++++- qa/common/check_copyright.py | 1 + 10 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 qa/L0_model_config/custom_parameters/tensorrt/invalid/allocation_strategy_invalid_value/expected create mode 100644 qa/L0_model_config/custom_parameters/tensorrt/invalid/allocation_strategy_invalid_value/partial.pbtxt create mode 100644 qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_no_key/partial.pbtxt create mode 100644 qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_no_parameters/partial.pbtxt create mode 100644 qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_1/expected create mode 100644 qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_1/partial.pbtxt create mode 100644 qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_2/expected create mode 100644 qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_2/partial.pbtxt diff --git a/qa/L0_model_config/custom_parameters/tensorrt/invalid/allocation_strategy_invalid_value/expected b/qa/L0_model_config/custom_parameters/tensorrt/invalid/allocation_strategy_invalid_value/expected new file mode 100644 index 0000000000..9e66bbafeb --- /dev/null +++ b/qa/L0_model_config/custom_parameters/tensorrt/invalid/allocation_strategy_invalid_value/expected @@ -0,0 +1 @@ +failed to load 'allocation_strategy_invalid_value' version 1: Invalid argument: Invalid value for 'execution_context_allocation_strategy': 'UNKNOWN' for model instance 'allocation_strategy_invalid_value' diff --git a/qa/L0_model_config/custom_parameters/tensorrt/invalid/allocation_strategy_invalid_value/partial.pbtxt b/qa/L0_model_config/custom_parameters/tensorrt/invalid/allocation_strategy_invalid_value/partial.pbtxt new file mode 100644 index 0000000000..2970c9bf23 --- /dev/null +++ b/qa/L0_model_config/custom_parameters/tensorrt/invalid/allocation_strategy_invalid_value/partial.pbtxt @@ -0,0 +1,31 @@ +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +parameters: { + key: "execution_context_allocation_strategy" + value: { + string_value: "UNKNOWN" + } +} diff --git a/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_no_key/partial.pbtxt b/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_no_key/partial.pbtxt new file mode 100644 index 0000000000..808432bbc5 --- /dev/null +++ b/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_no_key/partial.pbtxt @@ -0,0 +1,26 @@ +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +parameters: {} diff --git a/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_no_parameters/partial.pbtxt b/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_no_parameters/partial.pbtxt new file mode 100644 index 0000000000..ce81309581 --- /dev/null +++ b/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_no_parameters/partial.pbtxt @@ -0,0 +1,25 @@ +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_1/expected b/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_1/expected new file mode 100644 index 0000000000..584d6becdd --- /dev/null +++ b/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_1/expected @@ -0,0 +1 @@ +'execution_context_allocation_strategy' set to 'STATIC' for model instance 'allocation_strategy_value_1' diff --git a/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_1/partial.pbtxt b/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_1/partial.pbtxt new file mode 100644 index 0000000000..ebddccc5b1 --- /dev/null +++ b/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_1/partial.pbtxt @@ -0,0 +1,31 @@ +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +parameters: { + key: "execution_context_allocation_strategy" + value: { + string_value: "STATIC" + } +} diff --git a/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_2/expected b/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_2/expected new file mode 100644 index 0000000000..3dcd519ddf --- /dev/null +++ b/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_2/expected @@ -0,0 +1 @@ +'execution_context_allocation_strategy' set to 'ON_PROFILE_CHANGE' for model instance 'allocation_strategy_value_2' diff --git a/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_2/partial.pbtxt b/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_2/partial.pbtxt new file mode 100644 index 0000000000..11a75b4e9d --- /dev/null +++ b/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_2/partial.pbtxt @@ -0,0 +1,31 @@ +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +parameters: { + key: "execution_context_allocation_strategy" + value: { + string_value: "ON_PROFILE_CHANGE" + } +} diff --git a/qa/L0_model_config/test.sh b/qa/L0_model_config/test.sh index 7622cf93b2..4ef8f76907 100755 --- a/qa/L0_model_config/test.sh +++ b/qa/L0_model_config/test.sh @@ -298,7 +298,7 @@ cp -r /data/inferenceserver/${REPO_VERSION}/qa_model_repository/openvino_int8_in cp /data/inferenceserver/${REPO_VERSION}/qa_model_repository/openvino_int8_int8_int8/output0_labels.txt \ autofill_noplatform_success/openvino/partial_config -# Copy decoupled model into the model_metrics test repository. +# Copy decoupled model and config files into the model_metrics test repository. for modelpath in `ls -d model_metrics/*/*`; do src_dir="/opt/tritonserver/qa/python_models/async_execute_decouple" mkdir -p $modelpath/1 @@ -306,6 +306,18 @@ for modelpath in `ls -d model_metrics/*/*`; do cat $src_dir/config.pbtxt $modelpath/partial.pbtxt > $modelpath/config.pbtxt done +# Copy tensorrt model and config files into the custom_parameters test repository. +for modelpath in `ls -d custom_parameters/tensorrt/*/*`; do + mkdir -p $modelpath/1 + model_name=`basename $modelpath` + src_dir="/data/inferenceserver/${REPO_VERSION}/qa_model_repository/plan_float32_float32_float32" + cp ${src_dir}/1/model.plan $modelpath/1/. + cat ${src_dir}/config.pbtxt $modelpath/partial.pbtxt > $modelpath/config.pbtxt + sed -i "s/^name:.*/name: \"${model_name}\"/" $modelpath/config.pbtxt + sed -i "s/^version_policy:.*//" $modelpath/config.pbtxt + sed -i "s/label_filename:.*//" $modelpath/config.pbtxt +done + rm -f $SERVER_LOG_BASE* $CLIENT_LOG RET=0 @@ -694,6 +706,82 @@ for TARGET_DIR in `ls -d model_metrics/invalid_config/*`; do fi done +# Run all custom_parameters tests that are expected to succeed. +for TARGET_DIR in `ls -d custom_parameters/*/valid/*`; do + TARGET_DIR_DOT=`echo $TARGET_DIR | tr / .` + TARGET=`basename ${TARGET_DIR}` + + SERVER_ARGS="--model-repository=`pwd`/models --log-info=true" + SERVER_LOG=$SERVER_LOG_BASE.${TARGET_DIR_DOT}.log + + rm -fr models && mkdir models + cp -r ${TARGET_DIR} models/. + + EXPECTED=models/$TARGET/expected + echo -e "Test $TARGET_DIR" >> $CLIENT_LOG + + run_server + if [ "$SERVER_PID" == "0" ]; then + echo -e "*** FAILED: unable to start $SERVER" >> $CLIENT_LOG + RET=1 + else + kill $SERVER_PID + wait $SERVER_PID + fi + + if [ -f $EXPECTED ]; then + EX_FOUND=0 + EX=`cat $EXPECTED` + if grep ^I[0-9][0-9][0-9][0-9].*"$EX" $SERVER_LOG; then + echo -e "Found \"$EX\"" >> $CLIENT_LOG + EX_FOUND=1 + else + echo -e "Not found \"$EX\"" >> $CLIENT_LOG + fi + if [ "$EX_FOUND" == "0" ]; then + echo -e "*** FAILED: model_metrics/$TARGET" >> $CLIENT_LOG + RET=1 + fi + fi +done + +# Run all custom_parameters tests that have invalid values. +for TARGET_DIR in `ls -d custom_parameters/*/invalid/*`; do + TARGET_DIR_DOT=`echo $TARGET_DIR | tr / .` + TARGET=`basename ${TARGET_DIR}` + + SERVER_ARGS="--model-repository=`pwd`/models --log-info=true" + SERVER_LOG=$SERVER_LOG_BASE.${TARGET_DIR_DOT}.log + + rm -fr models && mkdir models + cp -r ${TARGET_DIR} models/. + + EXPECTED=models/$TARGET/expected + echo -e "Test $TARGET_DIR" >> $CLIENT_LOG + + # We expect all tests to fail with the expected error message + run_server + if [ "$SERVER_PID" != "0" ]; then + echo -e "*** FAILED: unexpected success starting $SERVER" >> $CLIENT_LOG + RET=1 + kill $SERVER_PID + wait $SERVER_PID + else + EX_FOUND=0 + EX=`cat $EXPECTED` + if grep ^E[0-9][0-9][0-9][0-9].*"$EX" $SERVER_LOG; then + echo -e "Found \"$EX\"" >> $CLIENT_LOG + EX_FOUND=1 + else + echo -e "Not found \"$EX\"" >> $CLIENT_LOG + fi + if [ "$EX_FOUND" == "0" ]; then + echo -e "*** FAILED: model_metrics/$TARGET" >> $CLIENT_LOG + RET=1 + fi + fi +done + if [ $RET -eq 0 ]; then echo -e "\n***\n*** Test Passed\n***" else diff --git a/qa/common/check_copyright.py b/qa/common/check_copyright.py index 99606a1c81..de3bdc79b4 100755 --- a/qa/common/check_copyright.py +++ b/qa/common/check_copyright.py @@ -75,6 +75,7 @@ "qa/L0_model_config/cli_messages/cli_override/expected", "qa/L0_model_config/cli_messages/cli_deprecation/expected", "qa/L0_model_config/model_metrics", + "qa/L0_model_config/custom_parameters", "qa/L0_model_namespacing/test_duplication", "qa/L0_model_namespacing/test_dynamic_resolution", "qa/L0_model_namespacing/test_ensemble_duplication", From 7dd581028897c2e7264d2940a208d775fc55c259 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Mon, 21 Apr 2025 09:30:17 -0700 Subject: [PATCH 020/230] TPRD-1425: Excluding Triton Model Analyzer from build (#8159) --- Dockerfile.sdk | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Dockerfile.sdk b/Dockerfile.sdk index de6b12df5e..3d35b66599 100644 --- a/Dockerfile.sdk +++ b/Dockerfile.sdk @@ -38,7 +38,6 @@ ARG TRITON_COMMON_REPO_TAG=main ARG TRITON_CORE_REPO_TAG=main ARG TRITON_CLIENT_REPO_TAG=main ARG TRITON_THIRD_PARTY_REPO_TAG=main -ARG TRITON_MODEL_ANALYZER_REPO_TAG=main ARG TRITON_ENABLE_GPU=ON ARG JAVA_BINDINGS_MAVEN_VERSION=3.8.4 ARG JAVA_BINDINGS_JAVACPP_PRESETS_TAG=1.5.8 @@ -261,11 +260,6 @@ RUN if [ "$TRITON_ENABLE_GPU" = "ON" ]; then \ RUN rm -f /usr/bin/python && \ ln -s /usr/bin/python3 /usr/bin/python -# Install Model Analyzer -ARG TRITON_MODEL_ANALYZER_REPO_TAG -ARG TRITON_MODEL_ANALYZER_REPO="${TRITON_REPO_ORGANIZATION}/model_analyzer@${TRITON_MODEL_ANALYZER_REPO_TAG}" -RUN pip3 install "git+${TRITON_MODEL_ANALYZER_REPO}" - # Entrypoint Banner ENV NVIDIA_PRODUCT_NAME="Triton Server SDK" COPY docker/entrypoint.d/ /opt/nvidia/entrypoint.d/ From 1f9787cbd56540849a44ff2207eb8be56ab2984f Mon Sep 17 00:00:00 2001 From: Olga Andreeva <124622579+oandreeva-nv@users.noreply.github.com> Date: Tue, 22 Apr 2025 12:41:16 -0700 Subject: [PATCH 021/230] Fixes for 25.04 release L0_grpc_* and L0_http_* tests (#8152) --- Dockerfile.QA | 19 ++++++------------- qa/L0_grpc/test.sh | 12 ++++++------ qa/L0_http/test.sh | 4 ++-- 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/Dockerfile.QA b/Dockerfile.QA index 7406cf2345..c9e9c74620 100644 --- a/Dockerfile.QA +++ b/Dockerfile.QA @@ -74,20 +74,13 @@ RUN apt update -q=2 \ && apt-get update -q=2 \ && apt-get install -y --no-install-recommends cmake=3.28.3* cmake-data=3.28.3* -# Add inception_graphdef model to example repo +# Add densenet_onnx model to example repo # FIXME: This should be changed to using the fetch_models.sh script # in order to ensure the public facing docs are up-to-date. WORKDIR /workspace/docs/examples/model_repository -RUN mkdir -p model_repository/inception_onnx/1 && \ - wget -O /tmp/inception_v3_2016_08_28_frozen.pb.tar.gz \ - https://storage.googleapis.com/download.tensorflow.org/models/inception_v3_2016_08_28_frozen.pb.tar.gz && \ - (cd /tmp && tar xzf inception_v3_2016_08_28_frozen.pb.tar.gz) && \ - python3 -m venv tf2onnx && \ - source ./tf2onnx/bin/activate && \ - pip3 install "numpy<2" tensorflow tf2onnx && \ - python3 -m tf2onnx.convert --graphdef /tmp/inception_v3_2016_08_28_frozen.pb --output inception_v3_onnx.model.onnx --inputs input:0 --outputs InceptionV3/Predictions/Softmax:0 && \ - deactivate && \ - mv inception_v3_onnx.model.onnx model_repository/inception_onnx/1/model.onnx +RUN mkdir -p densenet_onnx/1 && \ + wget -O densenet_onnx/1/model.onnx \ + https://github.com/onnx/models/raw/main/validated/vision/classification/densenet-121/model/densenet-7.onnx # Update the qa/ directory with test executables, models, etc. WORKDIR /workspace @@ -117,7 +110,7 @@ RUN mkdir -p qa/common && \ cp -r docs/examples/model_repository/simple_identity qa/L0_grpc/models && \ cp -r docs/examples/model_repository/simple_sequence qa/L0_grpc/models && \ cp -r docs/examples/model_repository/simple_string qa/L0_grpc/models && \ - cp -r docs/examples/model_repository/inception_onnx qa/L0_grpc/models && \ + cp -r docs/examples/model_repository/densenet_onnx qa/L0_grpc/models && \ mkdir qa/L0_grpc_state_cleanup/models && \ cp -r /workspace/src/test/models/repeat_int32 qa/L0_grpc_state_cleanup/models/ && \ mkdir qa/L0_http/models && \ @@ -126,7 +119,7 @@ RUN mkdir -p qa/common && \ cp -r docs/examples/model_repository/simple_identity qa/L0_http/models && \ cp -r docs/examples/model_repository/simple_sequence qa/L0_http/models && \ cp -r docs/examples/model_repository/simple_string qa/L0_http/models && \ - cp -r docs/examples/model_repository/inception_onnx qa/L0_grpc/models && \ + cp -r docs/examples/model_repository/densenet_onnx qa/L0_http/models && \ mkdir qa/L0_https/models && \ cp -r docs/examples/model_repository/simple qa/L0_https/models/. && \ mkdir qa/L0_secure_grpc/models && \ diff --git a/qa/L0_grpc/test.sh b/qa/L0_grpc/test.sh index 28d9236d0a..0302a0b0a4 100755 --- a/qa/L0_grpc/test.sh +++ b/qa/L0_grpc/test.sh @@ -205,19 +205,19 @@ for i in \ EXTRA_ARGS="-i grpc -u localhost:8001" fi if [[ ($SUFFIX == "image_client") || ($SUFFIX == "grpc_image_client") ]]; then - python $i -m inception_onnx -s INCEPTION -a -c 1 -b 1 $EXTRA_ARGS $IMAGE >> "${CLIENT_LOG}.async.${SUFFIX}" 2>&1 + python $i -m densenet_onnx -s INCEPTION -a -c 1 -b 1 $EXTRA_ARGS $IMAGE >> "${CLIENT_LOG}.async.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.async.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.async.${SUFFIX} RET=1 fi - python $i -m inception_onnx -s INCEPTION -a --streaming -c 1 -b 1 $EXTRA_ARGS $IMAGE >> "${CLIENT_LOG}.streaming.${SUFFIX}" 2>&1 + python $i -m densenet_onnx -s INCEPTION -a --streaming -c 1 -b 1 $EXTRA_ARGS $IMAGE >> "${CLIENT_LOG}.streaming.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.streaming.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.streaming.${SUFFIX} RET=1 fi - python $i -m inception_onnx -s INCEPTION -c 1 -b 1 $EXTRA_ARGS $IMAGE >> "${CLIENT_LOG}.${SUFFIX}" 2>&1 + python $i -m densenet_onnx -s INCEPTION -c 1 -b 1 $EXTRA_ARGS $IMAGE >> "${CLIENT_LOG}.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.${SUFFIX} @@ -270,19 +270,19 @@ for i in \ BASE=$(basename -- $i) SUFFIX="${BASE%.*}" if [[ $SUFFIX == "image_client" ]]; then - $i -m inception_onnx -s INCEPTION -a -c 1 -b 1 -i grpc -u localhost:8001 $IMAGE >> "${CLIENT_LOG}.c++.async.${SUFFIX}" 2>&1 + $i -m densenet_onnx -s INCEPTION -a -c 1 -b 1 -i grpc -u localhost:8001 $IMAGE >> "${CLIENT_LOG}.c++.async.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.c++.async.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.c++.${SUFFIX} RET=1 fi - $i -m inception_onnx -s INCEPTION -a --streaming -c 1 -b 1 -i grpc -u localhost:8001 $IMAGE >> "${CLIENT_LOG}.c++.streaming.${SUFFIX}" 2>&1 + $i -m densenet_onnx -s INCEPTION -a --streaming -c 1 -b 1 -i grpc -u localhost:8001 $IMAGE >> "${CLIENT_LOG}.c++.streaming.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.c++.streaming.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.c++.${SUFFIX} RET=1 fi - $i -m inception_onnx -s INCEPTION -c 1 -b 1 -i grpc -u localhost:8001 $IMAGE >> "${CLIENT_LOG}.c++.${SUFFIX}" 2>&1 + $i -m densenet_onnx -s INCEPTION -c 1 -b 1 -i grpc -u localhost:8001 $IMAGE >> "${CLIENT_LOG}.c++.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.c++.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.c++.${SUFFIX} diff --git a/qa/L0_http/test.sh b/qa/L0_http/test.sh index c36024e007..ace5cbd0f6 100755 --- a/qa/L0_http/test.sh +++ b/qa/L0_http/test.sh @@ -163,13 +163,13 @@ for i in \ BASE=$(basename -- $i) SUFFIX="${BASE%.*}" if [ $SUFFIX == "image_client" ]; then - python $i -m inception_onnx -s INCEPTION -a -c 1 -b 1 $IMAGE >> "${CLIENT_LOG}.async.${SUFFIX}" 2>&1 + python $i -m densenet_onnx -s INCEPTION -a -c 1 -b 1 $IMAGE >> "${CLIENT_LOG}.async.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.async.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.async.${SUFFIX} RET=1 fi - python $i -m inception_onnx -s INCEPTION -c 1 -b 1 $IMAGE >> "${CLIENT_LOG}.${SUFFIX}" 2>&1 + python $i -m densenet_onnx -s INCEPTION -c 1 -b 1 $IMAGE >> "${CLIENT_LOG}.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.${SUFFIX} From ff4bd4ef56ca431d39e7618b866ebb57458eb98e Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Wed, 23 Apr 2025 21:31:41 -0400 Subject: [PATCH 022/230] test: Input batch size overflow vulnerability (#8165) --- qa/L0_input_validation/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qa/L0_input_validation/test.sh b/qa/L0_input_validation/test.sh index 2d2cf515ce..d60bad8e79 100755 --- a/qa/L0_input_validation/test.sh +++ b/qa/L0_input_validation/test.sh @@ -151,7 +151,7 @@ kill $SERVER_PID wait $SERVER_PID # input_byte_size_test -cp -r /data/inferenceserver/${REPO_VERSION}/qa_identity_model_repository/{onnx_zero_1_float32,onnx_zero_1_object} ./models +cp -r /data/inferenceserver/${REPO_VERSION}/qa_identity_model_repository/{onnx_zero_1_float32,onnx_zero_1_object,onnx_zero_1_bool} ./models set +e LD_LIBRARY_PATH=/opt/tritonserver/lib:$LD_LIBRARY_PATH $TEST_EXEC >> $TEST_LOG 2>&1 From 5a2aaba23d87bd2411a093fa5aa659249378b10f Mon Sep 17 00:00:00 2001 From: mattwittwer Date: Sat, 26 Apr 2025 12:41:04 -0700 Subject: [PATCH 023/230] feat: Add graceful shutdown timer to GRPC frontend (#7969) Add shutdown timer to the gRPC endpoint for both infer and streaming infer requests. Inflight requests will be allowed to complete before and new requests made after shutdown has started will be rejected. --- qa/L0_lifecycle/lifecycle_test.py | 39 +++++++++---------- qa/L0_lifecycle/test.sh | 12 ++---- src/grpc/grpc_server.cc | 65 +++++++++++++++++++++++++++++-- src/grpc/grpc_server.h | 16 ++++++++ src/grpc/infer_handler.cc | 24 ++++++++---- src/grpc/infer_handler.h | 27 ++++++++++--- src/grpc/stream_infer_handler.cc | 25 ++++++++---- src/grpc/stream_infer_handler.h | 6 ++- src/main.cc | 19 ++++++--- 9 files changed, 174 insertions(+), 59 deletions(-) diff --git a/qa/L0_lifecycle/lifecycle_test.py b/qa/L0_lifecycle/lifecycle_test.py index 8ad95e3d90..2dba1c25d5 100755 --- a/qa/L0_lifecycle/lifecycle_test.py +++ b/qa/L0_lifecycle/lifecycle_test.py @@ -2669,7 +2669,8 @@ def callback(user_data, result, error): self.assertTrue(False, "expected error for new inference during shutdown") except InferenceServerException as ex: self.assertIn( - "Server is stopping, scheduler for model has stopped accepting new inference requests", + "failed to connect to all addresses; last error: UNKNOWN: ipv4:127.0.0.1:8001: " + + "Failed to connect to remote host: connect: Connection refused (111)", ex.message(), ) @@ -2731,33 +2732,29 @@ def callback(user_data, result, error): ) self.assertTrue(False, "expected error for new inference during shutdown") except InferenceServerException as ex: - self.assertIn( - "Server is stopping, scheduler for model has stopped accepting new inference requests", - ex.message(), - ) + # The first request received by the gRPC endpoint while shutting down returns CANCELLED + # each subsequent request returns Connection refused + self.assertIn("CANCELLED", ex.message()) # 2: New sequence with existing sequence ID try: triton_client.infer(model_name, inputs, sequence_id=1, sequence_start=True) self.assertTrue(False, "expected error for new inference during shutdown") except InferenceServerException as ex: self.assertIn( - "Server is stopping, scheduler for model has stopped accepting new inference requests", + "failed to connect to all addresses; last error: UNKNOWN: ipv4:127.0.0.1:8001: " + + "Failed to connect to remote host: connect: Connection refused (111)", ex.message(), ) - # 3: Continuing sequence + # 3: Continuing sequence after shutdown try: - res = triton_client.infer( - model_name, inputs, sequence_id=2, sequence_end=True - ) - output_data = res.as_numpy("OUTPUT") - # Result are accumulated - np.testing.assert_allclose( - output_data, - input_data + input_data, - err_msg="Inference result is not correct", + triton_client.infer(model_name, inputs, sequence_id=2, sequence_end=True) + self.assertTrue(False, "expected error for new inference during shutdown") + except InferenceServerException as ex: + self.assertIn( + "failed to connect to all addresses; last error: UNKNOWN: ipv4:127.0.0.1:8001: " + + "Failed to connect to remote host: connect: Connection refused (111)", + ex.message(), ) - except Exception as ex: - self.assertTrue(False, "unexpected error {}".format(ex)) # Wait until the results are available in user_data time_out = 30 @@ -2816,9 +2813,9 @@ def callback(user_data, result, error): triton_client.infer(model_name, inputs) self.assertTrue(False, "expected error for new inference during shutdown") except InferenceServerException as ex: - self.assertIn("in ensemble 'ensemble_zero_1_float32'", ex.message()) self.assertIn( - "Server is stopping, scheduler for model has stopped accepting new inference requests", + "failed to connect to all addresses; last error: UNKNOWN: ipv4:127.0.0.1:8001: " + + "Failed to connect to remote host: connect: Connection refused (111)", ex.message(), ) @@ -3388,7 +3385,7 @@ def test_shutdown_with_live_connection(self): # close connection conn.close() - time.sleep(2) + time.sleep(3) # check exit timeout countdown did not restart with open(os.environ["SERVER_LOG"]) as f: diff --git a/qa/L0_lifecycle/test.sh b/qa/L0_lifecycle/test.sh index bcd1713bb8..46dcf391b6 100755 --- a/qa/L0_lifecycle/test.sh +++ b/qa/L0_lifecycle/test.sh @@ -1579,8 +1579,8 @@ check_unit_test set -e # check server log -if [ `grep -c "Model 'custom_zero_1_float32' (version 1) has 1 in-flight inferences" $SERVER_LOG` == "0" ]; then - echo -e "\n***\n*** Expect logging for model and in-flight inference count\n***" +if [ `grep -c "Found 1 gRPC service connections and inference handlers" $SERVER_LOG` == "0" ]; then + echo -e "\n***\n*** Expect logging for in-flight gRPC connection count\n***" RET=1 fi @@ -1617,10 +1617,6 @@ if [ `grep -c "Model 'custom_sequence_int32' (version 1) has 2 in-flight inferen echo -e "\n***\n*** Expect logging for model having 2 in-flight inferences\n***" RET=1 fi -if [ `grep -c "Model 'custom_sequence_int32' (version 1) has 1 in-flight inferences" $SERVER_LOG` == "0" ]; then - echo -e "\n***\n*** Expect logging for model having 1 in-flight inference\n***" - RET=1 -fi kill $SERVER_PID || true wait $SERVER_PID @@ -1658,8 +1654,8 @@ check_unit_test set -e # check server log -if [ `grep -c "Model 'ensemble_zero_1_float32' (version 1) has 1 in-flight inferences" $SERVER_LOG` == "0" ]; then - echo -e "\n***\n*** Expect logging for model and in-flight inference count\n***" +if [ `grep -c "Found 1 gRPC service connections and inference handlers" $SERVER_LOG` == "0" ]; then + echo -e "\n***\n*** Expect logging for in-flight gRPC connection count\n***" RET=1 fi diff --git a/src/grpc/grpc_server.cc b/src/grpc/grpc_server.cc index 362c60e81e..3a28963c80 100644 --- a/src/grpc/grpc_server.cc +++ b/src/grpc/grpc_server.cc @@ -2394,7 +2394,8 @@ Server::Server( &service_, model_infer_cq_.get(), options.infer_allocation_pool_size_ /* max_state_bucket_count */, options.max_response_pool_size_, options.infer_compression_level_, - restricted_kv, options.forward_header_pattern_)); + restricted_kv, options.forward_header_pattern_, &conn_mtx_, &conn_cnt_, + &accepting_new_conn_)); } // Handler for streaming inference requests. Keeps one handler for streaming @@ -2404,7 +2405,8 @@ Server::Server( &service_, model_stream_infer_cq_.get(), options.infer_allocation_pool_size_ /* max_state_bucket_count */, options.max_response_pool_size_, options.infer_compression_level_, - restricted_kv, options.forward_header_pattern_)); + restricted_kv, options.forward_header_pattern_, &conn_mtx_, &conn_cnt_, + &accepting_new_conn_)); } Server::~Server() @@ -2561,6 +2563,31 @@ Server::Start() return nullptr; // success } +TRITONSERVER_Error* +Server::GracefulStop( + uint32_t* exit_timeout_secs, const std::string& service_name) +{ + if (!running_) { + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_UNAVAILABLE, "GRPC server is not running."); + } + + graceful_shutdown_thread_ = std::thread([this]() { + // Stop accepting new RPC requests. Existing requests are allowed to + // complete + server_->Shutdown(); + }); + + // Required to disable additional requests on existing streaming connections + DisableNewConnections(); + + if (exit_timeout_secs != nullptr) { + WaitForConnectionsToClose(exit_timeout_secs, service_name); + } + + return nullptr; // success +} + TRITONSERVER_Error* Server::Stop() { @@ -2569,9 +2596,14 @@ Server::Stop() TRITONSERVER_ERROR_UNAVAILABLE, "GRPC server is not running."); } - // Always shutdown the completion queue after the server. - server_->Shutdown(); + // Forcefully cancel remaining RPC connections + server_->Shutdown(std::chrono::system_clock::now()); + if (graceful_shutdown_thread_.joinable()) { + graceful_shutdown_thread_.join(); + } + + // Shutdown completion queues common_cq_->Shutdown(); model_infer_cq_->Shutdown(); model_stream_infer_cq_->Shutdown(); @@ -2590,4 +2622,29 @@ Server::Stop() return nullptr; // success } +TRITONSERVER_Error* +Server::DisableNewConnections() +{ + std::unique_lock lock(conn_mtx_); + + accepting_new_conn_ = false; + + return nullptr; // success +} + +TRITONSERVER_Error* +Server::WaitForConnectionsToClose( + uint32_t* exit_timeout_secs, const std::string& service_name) +{ + while (*exit_timeout_secs > 0 && conn_cnt_ > 0) { + LOG_INFO << "Timeout " << *exit_timeout_secs << ": Found " << conn_cnt_ + << " " << service_name + << " service connections and inference handlers"; + std::this_thread::sleep_for(std::chrono::seconds(1)); + (*exit_timeout_secs)--; + } + + return nullptr; // complete +} + }}} // namespace triton::server::grpc diff --git a/src/grpc/grpc_server.h b/src/grpc/grpc_server.h index a4fc358df0..6020bff7ad 100644 --- a/src/grpc/grpc_server.h +++ b/src/grpc/grpc_server.h @@ -27,6 +27,7 @@ #include +#include #include #include "../common.h" @@ -115,7 +116,13 @@ class Server { ~Server(); TRITONSERVER_Error* Start(); + TRITONSERVER_Error* GracefulStop( + uint32_t* exit_timeout_secs = nullptr, + const std::string& service_name = "gRPC"); TRITONSERVER_Error* Stop(); + TRITONSERVER_Error* DisableNewConnections(); + TRITONSERVER_Error* WaitForConnectionsToClose( + uint32_t* exit_timeout_secs, const std::string& service_name); private: Server( @@ -156,6 +163,15 @@ class Server { int bound_port_{0}; bool running_{false}; + + // Thread to handle the execution of the gRPC endpoint's graceful shutdown + std::thread graceful_shutdown_thread_; + // Mutex to protect access to the following connection variables + std::shared_mutex conn_mtx_; + // Counter to track the number of active connections and inference handlers + std::atomic conn_cnt_{0}; + // Flag to indicate if the server is currently accepting new connections + bool accepting_new_conn_{true}; }; }}} // namespace triton::server::grpc diff --git a/src/grpc/infer_handler.cc b/src/grpc/infer_handler.cc index 0bc1e9ce0b..6d3b6ddcf3 100644 --- a/src/grpc/infer_handler.cc +++ b/src/grpc/infer_handler.cc @@ -681,7 +681,7 @@ ModelInferHandler::Process( InferHandler::State* state, bool rpc_ok, bool is_notification) { // There are multiple handlers registered in the gRPC service. - // Hence, there we can have a case where a handler thread is + // Hence, we can have a case where a handler thread is // making progress in the state machine for a request and the // other thread is issuing cancellation on the same request. // Need to protect the state transitions for these cases. @@ -773,14 +773,24 @@ ModelInferHandler::Process( StartNewRequest(); } - if (ExecutePrecondition(state)) { + std::shared_lock lk1(*conn_mtx_); + + if (*accepting_new_conn_ && ExecutePrecondition(state)) { Execute(state); } else { - ::grpc::Status status = ::grpc::Status( - ::grpc::StatusCode::UNAVAILABLE, - std::string("This protocol is restricted, expecting header '") + - restricted_kv_.first + "'"); - + ::grpc::Status status; + if (*accepting_new_conn_) { + status = ::grpc::Status( + ::grpc::StatusCode::UNAVAILABLE, + "This protocol is restricted, expecting header '" + + restricted_kv_.first + "'"); + } else { + status = ::grpc::Status( + ::grpc::StatusCode::UNAVAILABLE, + "GRPC server is shutting down and has stopped accepting new " + "requests."); + } + lk1.unlock(); #ifdef TRITON_ENABLE_TRACING state->trace_timestamps_.emplace_back( diff --git a/src/grpc/infer_handler.h b/src/grpc/infer_handler.h index 40d5ce4806..e9bcef9eaf 100644 --- a/src/grpc/infer_handler.h +++ b/src/grpc/infer_handler.h @@ -32,6 +32,7 @@ #include #include #include +#include #include #include "../tracer.h" @@ -1290,7 +1291,8 @@ class InferHandler : public HandlerBase { ServiceType* service, ::grpc::ServerCompletionQueue* cq, size_t max_state_bucket_count, size_t max_response_queue_size, std::pair restricted_kv, - const std::string& header_forward_pattern); + const std::string& header_forward_pattern, std::shared_mutex* conn_mtx, + std::atomic* conn_cnt, bool* accepting_new_conn); virtual ~InferHandler(); // Descriptive name of of the handler. @@ -1303,6 +1305,9 @@ class InferHandler : public HandlerBase { void Stop() override; protected: + void IncrementConnectionCount() { conn_cnt_->fetch_add(1); } + void DecrementConnectionCount() { conn_cnt_->fetch_sub(1); } + using State = InferHandlerState; using StateContext = typename State::Context; @@ -1312,6 +1317,8 @@ class InferHandler : public HandlerBase { const std::shared_ptr& context, Steps start_step = Steps::START) { + IncrementConnectionCount(); + State* state = nullptr; if (max_state_bucket_count_ > 0) { @@ -1352,11 +1359,13 @@ class InferHandler : public HandlerBase { if (state_bucket_.size() < max_state_bucket_count_) { state->Release(); state_bucket_.push_back(state); + DecrementConnectionCount(); return; } } delete state; + DecrementConnectionCount(); } // Simple structure that carries the payload needed for @@ -1431,6 +1440,10 @@ class InferHandler : public HandlerBase { std::pair restricted_kv_; std::string header_forward_pattern_; re2::RE2 header_forward_regex_; + + std::shared_mutex* conn_mtx_; + std::atomic* conn_cnt_; + bool* accepting_new_conn_; }; template < @@ -1443,13 +1456,15 @@ InferHandler:: ServiceType* service, ::grpc::ServerCompletionQueue* cq, size_t max_state_bucket_count, size_t max_response_queue_size, std::pair restricted_kv, - const std::string& header_forward_pattern) + const std::string& header_forward_pattern, std::shared_mutex* conn_mtx, + std::atomic* conn_cnt, bool* accepting_new_conn) : name_(name), tritonserver_(tritonserver), service_(service), cq_(cq), max_state_bucket_count_(max_state_bucket_count), max_response_queue_size_(max_response_queue_size), restricted_kv_(restricted_kv), header_forward_pattern_(header_forward_pattern), - header_forward_regex_(header_forward_pattern_) + header_forward_regex_(header_forward_pattern_), conn_mtx_(conn_mtx), + conn_cnt_(conn_cnt), accepting_new_conn_(accepting_new_conn) { } @@ -1614,10 +1629,12 @@ class ModelInferHandler ::grpc::ServerCompletionQueue* cq, size_t max_state_bucket_count, size_t max_response_queue_size, grpc_compression_level compression_level, std::pair restricted_kv, - const std::string& forward_header_pattern) + const std::string& forward_header_pattern, std::shared_mutex* conn_mtx, + std::atomic* conn_cnt, bool* accepting_new_conn) : InferHandler( name, tritonserver, service, cq, max_state_bucket_count, - max_response_queue_size, restricted_kv, forward_header_pattern), + max_response_queue_size, restricted_kv, forward_header_pattern, + conn_mtx, conn_cnt, accepting_new_conn), trace_manager_(trace_manager), shm_manager_(shm_manager), compression_level_(compression_level) { diff --git a/src/grpc/stream_infer_handler.cc b/src/grpc/stream_infer_handler.cc index c87367546b..3789e5f9e8 100644 --- a/src/grpc/stream_infer_handler.cc +++ b/src/grpc/stream_infer_handler.cc @@ -232,9 +232,20 @@ ModelStreamInferHandler::Process( return !finished; } + std::shared_lock lk1(*conn_mtx_); + + if (!*accepting_new_conn_) { + err = TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_UNAVAILABLE, + "GRPC server is shutting down and has stopped accepting new " + "requests."); + } + int64_t requested_model_version; - err = GetModelVersionFromString( - request.model_version(), &requested_model_version); + if (err == nullptr) { + err = GetModelVersionFromString( + request.model_version(), &requested_model_version); + } // Record the transaction policy of the model into the current state // object. @@ -394,9 +405,9 @@ ModelStreamInferHandler::Process( state->context_->WriteResponseIfReady(state); } else { InferHandler::State* writing_state = nullptr; - std::lock_guard lk1(state->context_->mu_); + std::lock_guard lk2(state->context_->mu_); { - std::lock_guard lk2(state->step_mtx_); + std::lock_guard lk3(state->step_mtx_); state->response_queue_->MarkNextResponseComplete(); state->context_->ready_to_write_states_.push(state); if (!state->context_->ongoing_write_) { @@ -525,9 +536,9 @@ ModelStreamInferHandler::Process( { InferHandler::State* writing_state = nullptr; - std::lock_guard lk1(state->context_->mu_); + std::lock_guard lk2(state->context_->mu_); { - std::lock_guard lk2(state->step_mtx_); + std::lock_guard lk3(state->step_mtx_); if (!state->context_->ready_to_write_states_.empty()) { writing_state = state->context_->ready_to_write_states_.front(); state->context_->ready_to_write_states_.pop(); @@ -552,7 +563,7 @@ ModelStreamInferHandler::Process( } else if (state->step_ == Steps::WRITEREADY) { // Finish the state if all the transactions associated with // the state have completed. - std::lock_guard lk1(state->context_->mu_); + std::lock_guard lk2(state->context_->mu_); { if (state->IsComplete()) { state->context_->DecrementRequestCounter(); diff --git a/src/grpc/stream_infer_handler.h b/src/grpc/stream_infer_handler.h index 4be275777c..ac2acdbaa3 100644 --- a/src/grpc/stream_infer_handler.h +++ b/src/grpc/stream_infer_handler.h @@ -73,10 +73,12 @@ class ModelStreamInferHandler ::grpc::ServerCompletionQueue* cq, size_t max_state_bucket_count, size_t max_response_queue_size, grpc_compression_level compression_level, std::pair restricted_kv, - const std::string& header_forward_pattern) + const std::string& header_forward_pattern, std::shared_mutex* conn_mtx, + std::atomic* conn_cnt, bool* accepting_new_conn) : InferHandler( name, tritonserver, service, cq, max_state_bucket_count, - max_response_queue_size, restricted_kv, header_forward_pattern), + max_response_queue_size, restricted_kv, header_forward_pattern, + conn_mtx, conn_cnt, accepting_new_conn), trace_manager_(trace_manager), shm_manager_(shm_manager), compression_level_(compression_level) { diff --git a/src/main.cc b/src/main.cc index b2eee17c68..db558d45bf 100644 --- a/src/main.cc +++ b/src/main.cc @@ -1,4 +1,4 @@ -// Copyright 2018-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -316,6 +316,17 @@ StopEndpoints(uint32_t* exit_timeout_secs) } #endif // TRITON_ENABLE_HTTP +#ifdef TRITON_ENABLE_GRPC + // Allow for graceful shutdown of GRPC service + if (g_grpc_service) { + TRITONSERVER_Error* err = g_grpc_service->GracefulStop(exit_timeout_secs); + if (err != nullptr) { + LOG_TRITONSERVER_ERROR(err, "failed to gracefully stop GRPC service"); + ret = false; + } + } +#endif // TRITON_ENABLE_GRPC + return ret; } @@ -324,11 +335,9 @@ StopEndpoints() { bool ret = true; - // TODO: Add support for 'exit_timeout_secs' to the endpoints below and move - // them to the 'StopEndpoints(uint32_t* exit_timeout_secs)' function above. - #ifdef TRITON_ENABLE_GRPC if (g_grpc_service) { + // Forceful shutdown of GRPC service TRITONSERVER_Error* err = g_grpc_service->Stop(); if (err != nullptr) { LOG_TRITONSERVER_ERROR(err, "failed to stop GRPC service"); @@ -521,7 +530,7 @@ main(int argc, char** argv) triton::server::signal_exit_cv_.wait_for(lock, wait_timeout); } - // Stop the HTTP[, gRPC, and metrics] endpoints, and update exit timeout. + // Stop the HTTP and gRPC endpoints, and update exit timeout. uint32_t exit_timeout_secs = g_triton_params.exit_timeout_secs_; StopEndpoints(&exit_timeout_secs); TRITONSERVER_ServerSetExitTimeout(server_ptr, exit_timeout_secs); From d308c25876e53d086b2d05d14d3ebe809dbaa212 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:55:59 -0700 Subject: [PATCH 024/230] Adding additional output to build process (#8175) --- build.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.py b/build.py index 8d66b68027..61f5a94121 100755 --- a/build.py +++ b/build.py @@ -323,7 +323,7 @@ def gitclone(self, repo, tag, subdir, org): # reference onto a new branch we name "tritonbuildref". if tag.startswith("pull/"): self.cmd( - f" git clone --recursive --depth=1 {org}/{repo}.git {subdir};", + f" git clone --recursive --depth=1 {org}/{repo}.git {subdir}; git --git-dir {subdir}/.git log --oneline -1", check_exitcode=True, ) self.cmd("}" if target_platform() == "windows" else "fi") @@ -332,7 +332,7 @@ def gitclone(self, repo, tag, subdir, org): self.cmd(f"git checkout tritonbuildref", check_exitcode=True) else: self.cmd( - f" git clone --recursive --single-branch --depth=1 -b {tag} {org}/{repo}.git {subdir};", + f" git clone --recursive --single-branch --depth=1 -b {tag} {org}/{repo}.git {subdir}; git --git-dir {subdir}/.git log --oneline -1", check_exitcode=True, ) self.cmd("}" if target_platform() == "windows" else "fi") From 744995f3ead2422c5e0ba74332d96634bdb3d0b7 Mon Sep 17 00:00:00 2001 From: Pavithra Vijayakrishnan <160681768+pvijayakrish@users.noreply.github.com> Date: Tue, 29 Apr 2025 16:48:24 -0700 Subject: [PATCH 025/230] build: Integrate to use PA and GAP assets if available (#8155) --- Dockerfile.sdk | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/Dockerfile.sdk b/Dockerfile.sdk index 3d35b66599..510ed537b9 100644 --- a/Dockerfile.sdk +++ b/Dockerfile.sdk @@ -41,7 +41,7 @@ ARG TRITON_THIRD_PARTY_REPO_TAG=main ARG TRITON_ENABLE_GPU=ON ARG JAVA_BINDINGS_MAVEN_VERSION=3.8.4 ARG JAVA_BINDINGS_JAVACPP_PRESETS_TAG=1.5.8 - +ARG TRITON_PERF_ANALYZER_BUILD=1 # DCGM version to install for Model Analyzer ARG DCGM_VERSION=3.3.6 @@ -116,6 +116,8 @@ ARG JAVA_BINDINGS_MAVEN_VERSION ARG JAVA_BINDINGS_JAVACPP_PRESETS_TAG ARG TARGETPLATFORM +ENV TRITON_PERF_ANALYZER_BUILD=${TRITON_PERF_ANALYZER_BUILD} + WORKDIR /workspace COPY TRITON_VERSION . COPY ${TRITON_CLIENT_REPO_SUBDIR} client @@ -144,7 +146,10 @@ RUN make -j16 cc-clients java-clients && \ # the python client until now. Post-migration we should focus # effort on de-tangling these flows. WORKDIR /workspace/pa_build -RUN cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ +# NOTE: If TRITON_PERF_ANALYZER_BUILD=0, the Performance Analyzer (PA) binaries must already exist +# in the path specified by the ARG TRITON_PA_REPO_SUBDIR. +RUN if [ "$TRITON_PERF_ANALYZER_BUILD" = "1" ]; then \ + cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ -DTRITON_VERSION=`cat /workspace/TRITON_VERSION` \ -DTRITON_REPO_ORGANIZATION=${TRITON_REPO_ORGANIZATION} \ -DTRITON_COMMON_REPO_TAG=${TRITON_COMMON_REPO_TAG} \ @@ -160,12 +165,29 @@ RUN cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ -DTRITON_ENABLE_PYTHON_GRPC=ON \ -DTRITON_PACKAGE_PERF_ANALYZER=ON \ -DTRITON_ENABLE_GPU=${TRITON_ENABLE_GPU} \ - /workspace/perf_analyzer -RUN make -j16 perf-analyzer python-clients - -RUN pip3 install build \ - && cd /workspace/perf_analyzer/genai-perf \ - && python3 -m build --wheel --outdir /workspace/install/python + /workspace/perf_analyzer && \ + make -j16 perf-analyzer python-clients && \ + pip3 install build && \ + cd /workspace/perf_analyzer/genai-perf && \ + python3 -m build --wheel --outdir /workspace/install/python; \ + else \ + ls /workspace/perf_analyzer/ && \ + tar -xzf /workspace/perf_analyzer/perf_analyzer*.tar.gz -C /workspace/install/bin && \ + echo "Perf Analyzer binaries was extracted and not build" && \ + cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ + -DTRITON_VERSION=`cat /workspace/TRITON_VERSION` \ + -DTRITON_REPO_ORGANIZATION=${TRITON_REPO_ORGANIZATION} \ + -DTRITON_COMMON_REPO_TAG=${TRITON_COMMON_REPO_TAG} \ + -DTRITON_CLIENT_REPO_TAG=${TRITON_CLIENT_REPO_TAG} \ + -DTRITON_ENABLE_PYTHON_HTTP=ON \ + -DTRITON_ENABLE_PYTHON_GRPC=ON \ + -DTRITON_PACKAGE_PERF_ANALYZER=ON \ + -DTRITON_ENABLE_GPU=${TRITON_ENABLE_GPU} \ + /workspace/perf_analyzer && \ + make -j16 python-clients && \ + mkdir -p /workspace/install/python && \ + cp /workspace/perf_analyzer/genai_perf-*.whl /workspace/install/python/; \ + fi # Install Java API Bindings RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ From 02706fd3fb90a598c15e3c8877386d91fb1307c1 Mon Sep 17 00:00:00 2001 From: Pavithra Vijayakrishnan <160681768+pvijayakrish@users.noreply.github.com> Date: Wed, 30 Apr 2025 11:56:21 -0700 Subject: [PATCH 026/230] build: Update ARG in Dockerfile.sdk (#8179) --- Dockerfile.sdk | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile.sdk b/Dockerfile.sdk index 510ed537b9..15b164b20e 100644 --- a/Dockerfile.sdk +++ b/Dockerfile.sdk @@ -115,6 +115,7 @@ ARG TRITON_ENABLE_GPU ARG JAVA_BINDINGS_MAVEN_VERSION ARG JAVA_BINDINGS_JAVACPP_PRESETS_TAG ARG TARGETPLATFORM +ARG TRITON_PERF_ANALYZER_BUILD ENV TRITON_PERF_ANALYZER_BUILD=${TRITON_PERF_ANALYZER_BUILD} From 8d62bd88f6cfa737eb09de29a5a1333b511278b2 Mon Sep 17 00:00:00 2001 From: mattwittwer Date: Thu, 1 May 2025 20:15:32 -0700 Subject: [PATCH 027/230] Fix: Update handling of shared mem integer values (#8170) Added an additional check to prevent the value of byte_size and offset used in a request from exceeding the bounds of shared memory. --- qa/L0_shared_memory/shared_memory_test.py | 55 ++++++++++++++++++++++- qa/L0_shared_memory/test.sh | 3 +- src/shared_memory_manager.cc | 15 ++++++- 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/qa/L0_shared_memory/shared_memory_test.py b/qa/L0_shared_memory/shared_memory_test.py index 8f3c2fbb52..146025cf26 100755 --- a/qa/L0_shared_memory/shared_memory_test.py +++ b/qa/L0_shared_memory/shared_memory_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -385,6 +385,59 @@ def test_infer_byte_size_out_of_bound(self): ) self._cleanup_shm_handles() + def test_infer_integer_overflow(self): + # Test for integer overflow vulnerability in offset + byte_size calculation + error_msg = [] + self._configure_server() + + offset = 32 + byte_size = 2**64 - 32 + + if self.protocol == "http": + iu.shm_basic_infer( + self, + self.triton_client, + self._shm_handles[0], + self._shm_handles[1], + self._shm_handles[2], + self._shm_handles[3], + error_msg, + shm_output_offset=offset, + shm_output_byte_size=byte_size, + protocol=self.protocol, + use_system_shared_memory=True, + ) + + self.assertEqual(len(error_msg), 1) + self.assertTrue( + "Integer overflow detected: byte_size " in error_msg[0], + f"Unexpected error message: {error_msg[0]}", + ) + self._cleanup_shm_handles() + else: + # The gRPC client utilizes the int64_param and will throw a separate error for values larger than 2**63-1 + try: + iu.shm_basic_infer( + self, + self.triton_client, + self._shm_handles[0], + self._shm_handles[1], + self._shm_handles[2], + self._shm_handles[3], + error_msg, + shm_output_offset=offset, + shm_output_byte_size=byte_size, + protocol=self.protocol, + use_system_shared_memory=True, + ) + self.assertTrue( + False, + "Expected gRPC client to fail on value larger than int64_param maximum", + ) + except ValueError as ex: + self.assertIn("Value out of range:", str(ex)) + self._cleanup_shm_handles() + def test_register_out_of_bound(self): create_byte_size = self.DEFAULT_SHM_BYTE_SIZE diff --git a/qa/L0_shared_memory/test.sh b/qa/L0_shared_memory/test.sh index 27e8b2dd80..97b81397ec 100755 --- a/qa/L0_shared_memory/test.sh +++ b/qa/L0_shared_memory/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -53,6 +53,7 @@ for i in \ test_unregisterall \ test_infer_offset_out_of_bound \ test_infer_byte_size_out_of_bound \ + test_infer_integer_overflow \ test_register_out_of_bound \ test_python_client_leak; do for client_type in http grpc; do diff --git a/src/shared_memory_manager.cc b/src/shared_memory_manager.cc index e1966f79c8..17b0e43da0 100644 --- a/src/shared_memory_manager.cc +++ b/src/shared_memory_manager.cc @@ -1,4 +1,4 @@ -// Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -485,6 +485,19 @@ SharedMemoryManager::GetMemoryInfo( std::string("Invalid offset for shared memory region: '" + name + "'") .c_str()); } + + // Check for potential integer overflow before validating bounds + if (byte_size > (SIZE_MAX - offset)) { + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + std::string( + "Integer overflow detected: byte_size (" + + std::to_string(byte_size) + ") + offset (" + + std::to_string(offset) + ") exceeds maximum value (" + + std::to_string(SIZE_MAX) + ") for region '" + name + "'") + .c_str()); + } + // validate byte_size + offset is within memory bounds size_t total_req_shm = offset + byte_size - 1; if (total_req_shm > shm_region_end) { From f46557bec4af292d111fa2061695c6c193710206 Mon Sep 17 00:00:00 2001 From: Sai Kiran Polisetty Date: Fri, 2 May 2025 19:18:23 +0530 Subject: [PATCH 028/230] fix: Add HTTP JSON parsing recursion depth limit (#8172) --- qa/L0_http/http_test.py | 51 ++++++++++++++++++++++++++++++++++++++++- qa/L0_http/test.sh | 8 ++++++- src/common.h | 5 +++- src/http_server.cc | 29 +++++++++++++++++++---- 4 files changed, 85 insertions(+), 8 deletions(-) diff --git a/qa/L0_http/http_test.py b/qa/L0_http/http_test.py index 638ccbbbf8..a838283e0d 100755 --- a/qa/L0_http/http_test.py +++ b/qa/L0_http/http_test.py @@ -1,5 +1,5 @@ #!/usr/bin/python -# Copyright 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -273,6 +273,55 @@ def test_loading_large_invalid_model(self): except ValueError: self.fail("Response is not valid JSON") + def test_json_recursion_depth_limit(self): + """Test that server properly handles and rejects deeply nested JSON.""" + + def create_nested_json(depth, value): + for _ in range(depth): + value = f"[{value}]" + return json.loads(value) + + headers = {"Content-Type": "application/json"} + test_matrix = [ + # (datatype, data, model, json_depth, should_succeed) + ("BYTES", '"hello"', "simple_identity", 120, False), + ("BYTES", '"hello"', "simple_identity", 50, True), + ("INT64", "123", "simple_identity_int64", 120, False), + ("INT64", "123", "simple_identity_int64", 50, True), + ] + + for dtype, data, model, json_depth, should_succeed in test_matrix: + with self.subTest( + datatype=dtype, depth=json_depth, should_succeed=should_succeed + ): + payload = { + "inputs": [ + { + "name": "INPUT0", + "datatype": dtype, + "shape": [1, 1], + "data": create_nested_json(json_depth, data), + } + ] + } + + response = requests.post( + self._get_infer_url(model), headers=headers, json=payload + ) + + if should_succeed: + self.assertEqual(response.status_code, 200) + else: + self.assertNotEqual(response.status_code, 200) + try: + error_message = response.json().get("error", "") + self.assertIn( + "JSON nesting depth exceeds maximum allowed limit (100)", + error_message, + ) + except ValueError: + self.fail("Response is not valid JSON") + if __name__ == "__main__": unittest.main() diff --git a/qa/L0_http/test.sh b/qa/L0_http/test.sh index ace5cbd0f6..1146523036 100755 --- a/qa/L0_http/test.sh +++ b/qa/L0_http/test.sh @@ -612,6 +612,12 @@ cp -r ${MODELDIR}/onnx_zero_1_float32 ${MODELDIR}/onnx_zero_1_float32_queue && \ echo " }" >> config.pbtxt && \ echo "}" >> config.pbtxt) +cp -r ./models/simple_identity ${MODELDIR} +cp -r ./models/simple_identity ${MODELDIR}/simple_identity_int64 && \ + (cd $MODELDIR/simple_identity_int64 && \ + sed -i "s/TYPE_STRING/TYPE_INT64/" config.pbtxt && \ + sed -i "s/simple_identity/simple_identity_int64/" config.pbtxt) + SERVER_ARGS="--backend-directory=${BACKEND_DIR} --model-repository=${MODELDIR}" SERVER_LOG="./inference_server_http_test.log" CLIENT_LOG="./http_test.log" @@ -624,7 +630,7 @@ fi TEST_RESULT_FILE='test_results.txt' PYTHON_TEST=http_test.py -EXPECTED_NUM_TESTS=10 +EXPECTED_NUM_TESTS=11 set +e python $PYTHON_TEST >$CLIENT_LOG 2>&1 if [ $? -ne 0 ]; then diff --git a/src/common.h b/src/common.h index fb8f9fdee6..02e9e1a890 100644 --- a/src/common.h +++ b/src/common.h @@ -1,4 +1,4 @@ -// Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -51,6 +51,9 @@ constexpr int MAX_GRPC_MESSAGE_SIZE = INT32_MAX; /// dimension can take on any size. constexpr int WILDCARD_DIM = -1; +// Maximum allowed depth for JSON parsing +constexpr int32_t HTTP_MAX_JSON_NESTING_DEPTH = 100; + /// Request parameter keys that start with a "triton_" prefix for internal use const std::vector TRITON_RESERVED_REQUEST_PARAMS{ "triton_enable_empty_final_response"}; diff --git a/src/http_server.cc b/src/http_server.cc index edec3aae0a..b08a1f5ccb 100644 --- a/src/http_server.cc +++ b/src/http_server.cc @@ -437,8 +437,18 @@ AllocEVBuffer(const size_t byte_size, evbuffer** evb, void** base) // Recursively adds to byte_size from multi dimensional data input TRITONSERVER_Error* JsonBytesArrayByteSize( - triton::common::TritonJson::Value& tensor_data, size_t* byte_size) + triton::common::TritonJson::Value& tensor_data, size_t* byte_size, + int current_depth = 0) { + if (current_depth >= HTTP_MAX_JSON_NESTING_DEPTH) { + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + ("JSON nesting depth exceeds maximum allowed " + "limit (" + + std::to_string(HTTP_MAX_JSON_NESTING_DEPTH) + ")") + .c_str()); + } + *byte_size = 0; // Recurse if not last dimension... if (tensor_data.IsArray()) { @@ -446,7 +456,7 @@ JsonBytesArrayByteSize( triton::common::TritonJson::Value el; RETURN_IF_ERR(tensor_data.At(i, &el)); size_t byte_size_; - RETURN_IF_ERR(JsonBytesArrayByteSize(el, &byte_size_)); + RETURN_IF_ERR(JsonBytesArrayByteSize(el, &byte_size_, current_depth + 1)); *byte_size += byte_size_; } } else { @@ -466,20 +476,29 @@ TRITONSERVER_Error* ReadDataFromJsonHelper( char* base, const TRITONSERVER_DataType dtype, triton::common::TritonJson::Value& tensor_data, int* counter, - int64_t expected_cnt) + int64_t expected_cnt, int current_depth = 0) { // FIXME should move 'switch' statement outside the recursive function and // pass in a read data callback once data type is confirmed. // Currently 'switch' is performed on each element even through all elements // have the same data type. + if (current_depth >= HTTP_MAX_JSON_NESTING_DEPTH) { + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + ("JSON nesting depth exceeds maximum allowed " + "limit (" + + std::to_string(HTTP_MAX_JSON_NESTING_DEPTH) + ")") + .c_str()); + } + // Recurse on array element if not last dimension... if (tensor_data.IsArray()) { for (size_t i = 0; i < tensor_data.ArraySize(); i++) { triton::common::TritonJson::Value el; RETURN_IF_ERR(tensor_data.At(i, &el)); - RETURN_IF_ERR( - ReadDataFromJsonHelper(base, dtype, el, counter, expected_cnt)); + RETURN_IF_ERR(ReadDataFromJsonHelper( + base, dtype, el, counter, expected_cnt, current_depth + 1)); } } else { // Check if writing to 'serialized' is overrunning the expected byte_size From 58ccf5157c40b8ec80b4f963523ffba947186221 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Wed, 7 May 2025 07:55:11 -0400 Subject: [PATCH 029/230] test: Add backend_api_test to test backend APIs (#8185) --- Dockerfile.QA | 1 + qa/L0_input_validation/test.sh | 17 +- src/test/CMakeLists.txt | 41 ++- src/test/backend_tensor_size_test.cc | 413 +++++++++++++++++++++++++++ 4 files changed, 469 insertions(+), 3 deletions(-) create mode 100644 src/test/backend_tensor_size_test.cc diff --git a/Dockerfile.QA b/Dockerfile.QA index c9e9c74620..d95b6cf9e2 100644 --- a/Dockerfile.QA +++ b/Dockerfile.QA @@ -144,6 +144,7 @@ RUN mkdir -p qa/common && \ mkdir qa/L0_data_compression/models && \ cp -r docs/examples/model_repository/simple qa/L0_data_compression/models && \ cp bin/data_compressor_test qa/L0_data_compression/. && \ + cp bin/backend_tensor_size_test qa/L0_input_validation/. && \ cp bin/metrics_api_test qa/L0_metrics/. && \ cp bin/response_cache_test qa/L0_response_cache/. && \ cp bin/request_cancellation_test qa/L0_request_cancellation/. && \ diff --git a/qa/L0_input_validation/test.sh b/qa/L0_input_validation/test.sh index d60bad8e79..699fa97b02 100755 --- a/qa/L0_input_validation/test.sh +++ b/qa/L0_input_validation/test.sh @@ -48,8 +48,6 @@ CLIENT_LOG="./input_validation_client.log" TEST_PY=./input_validation_test.py TEST_RESULT_FILE='./test_results.txt' SERVER_LOG="./inference_server.log" -TEST_LOG="./input_byte_size_test.log" -TEST_EXEC=./input_byte_size_test export CUDA_VISIBLE_DEVICES=0 @@ -151,6 +149,8 @@ kill $SERVER_PID wait $SERVER_PID # input_byte_size_test +TEST_LOG="./input_byte_size_test.log" +TEST_EXEC=./input_byte_size_test cp -r /data/inferenceserver/${REPO_VERSION}/qa_identity_model_repository/{onnx_zero_1_float32,onnx_zero_1_object,onnx_zero_1_bool} ./models set +e @@ -162,6 +162,19 @@ if [ $? -ne 0 ]; then fi set -e +# backend_tensor_size_test +TEST_LOG="./backend_tensor_size_test.log" +TEST_EXEC=./backend_tensor_size_test + +set +e +LD_LIBRARY_PATH=/opt/tritonserver/lib:$LD_LIBRARY_PATH $TEST_EXEC >> $TEST_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $TEST_LOG + echo -e "\n***\n*** backend_tensor_size_test FAILED\n***" + RET=1 +fi +set -e + if [ $RET -eq 0 ]; then echo -e "\n***\n*** Input Validation Test Passed\n***" else diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index ab8686919d..23243bf9d9 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -77,6 +77,45 @@ if(${TRITON_ENABLE_HTTP} OR ${TRITON_ENABLE_METRICS} OR ) endif() +# +# Unit test for Backend API +# +add_executable( + backend_tensor_size_test + backend_tensor_size_test.cc +) + +set_target_properties( + backend_tensor_size_test + PROPERTIES + SKIP_BUILD_RPATH TRUE + BUILD_WITH_INSTALL_RPATH TRUE + INSTALL_RPATH_USE_LINK_PATH FALSE + INSTALL_RPATH "" +) + +target_include_directories( + backend_tensor_size_test + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/.. + ${GTEST_INCLUDE_DIRS} +) + +target_link_libraries( + backend_tensor_size_test + PRIVATE + triton-backend-utils # from repo-backend + triton-core-serverapi # from repo-core + triton-core-serverstub # from repo-core + GTest::gtest + GTest::gtest_main +) + +install( + TARGETS backend_tensor_size_test + RUNTIME DESTINATION bin +) + add_subdirectory(repoagent/relocation_repoagent repoagent/relocation_repoagent) add_subdirectory(distributed_addsub distributed_addsub) diff --git a/src/test/backend_tensor_size_test.cc b/src/test/backend_tensor_size_test.cc new file mode 100644 index 0000000000..22a700c40a --- /dev/null +++ b/src/test/backend_tensor_size_test.cc @@ -0,0 +1,413 @@ +// Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of NVIDIA CORPORATION nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +// OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "gtest/gtest.h" + +// Undefine the FAIL() macro inside Triton code to avoid redefine error +// from gtest. Okay as FAIL() is not used in data_compressor +#ifdef FAIL +#undef FAIL +#endif + +#include +#include + +#include "triton/backend/backend_common.h" +#include "triton/core/tritonserver.h" + +namespace tb = triton::backend; + +namespace { + +struct TritonServerError { + TritonServerError(TRITONSERVER_Error_Code code, const char* msg) + : code_(code), msg_(msg) + { + } + TRITONSERVER_Error_Code code_; + std::string msg_; +}; + +} // namespace + +#ifdef __cplusplus +extern "C" { +#endif + +TRITONSERVER_Error_Code +TRITONSERVER_ErrorCode(TRITONSERVER_Error* error) +{ + return (reinterpret_cast(error))->code_; +} + +const char* +TRITONSERVER_ErrorMessage(TRITONSERVER_Error* error) +{ + return (reinterpret_cast(error))->msg_.c_str(); +} + +#ifdef __cplusplus +} +#endif + +namespace { + +enum class ErrorCode { + kInvalidDim = -2, + kOverflow = -3, +}; + +void +assert_get_element_count_success( + std::vector& shape, int64_t expected_cnt) +{ + int64_t cnt; + TRITONSERVER_Error* err; + + // assert old APIs + ASSERT_EQ(expected_cnt, tb::GetElementCount(shape.data(), shape.size())); + ASSERT_EQ(expected_cnt, tb::GetElementCount(shape)); + + // assert new APIs + err = tb::GetElementCount(shape.data(), shape.size(), &cnt); + ASSERT_EQ(err, nullptr); + ASSERT_EQ(cnt, expected_cnt); + err = tb::GetElementCount(shape, &cnt); + ASSERT_EQ(err, nullptr); + ASSERT_EQ(cnt, expected_cnt); +} + +void +assert_get_element_count_error( + std::vector& shape, ErrorCode error_code, + const std::string& error_msg) +{ + int64_t cnt; + TRITONSERVER_Error* err; + + // assert old APIs + ASSERT_EQ( + static_cast(error_code), + tb::GetElementCount(shape.data(), shape.size())); + ASSERT_EQ(static_cast(error_code), tb::GetElementCount(shape)); + + // assert new APIs + err = tb::GetElementCount(shape.data(), shape.size(), &cnt); + ASSERT_NE(err, nullptr); + ASSERT_EQ(TRITONSERVER_ERROR_INVALID_ARG, TRITONSERVER_ErrorCode(err)); + ASSERT_STREQ(error_msg.c_str(), TRITONSERVER_ErrorMessage(err)); + err = tb::GetElementCount(shape, &cnt); + ASSERT_NE(err, nullptr); + ASSERT_EQ(TRITONSERVER_ERROR_INVALID_ARG, TRITONSERVER_ErrorCode(err)); + ASSERT_STREQ(error_msg.c_str(), TRITONSERVER_ErrorMessage(err)); +} + +void +assert_get_byte_size_success( + TRITONSERVER_DataType dtype, std::vector& shape, + int64_t expected_size) +{ + int64_t size; + TRITONSERVER_Error* err; + + // assert old API + ASSERT_EQ(expected_size, tb::GetByteSize(dtype, shape)); + + // assert new API + err = tb::GetByteSize(dtype, shape, &size); + ASSERT_EQ(err, nullptr); + ASSERT_EQ(expected_size, size); +} + +void +assert_get_byte_size_error( + TRITONSERVER_DataType dtype, std::vector& shape, + ErrorCode error_code, const std::string& error_msg) +{ + int64_t size; + TRITONSERVER_Error* err; + + // assert old API + ASSERT_EQ(static_cast(error_code), tb::GetByteSize(dtype, shape)); + + // assert new API + err = tb::GetByteSize(dtype, shape, &size); + ASSERT_NE(err, nullptr); + ASSERT_EQ(TRITONSERVER_ERROR_INVALID_ARG, TRITONSERVER_ErrorCode(err)); + ASSERT_EQ(error_msg, TRITONSERVER_ErrorMessage(err)); +} + +class GetElementCountTest : public ::testing::Test { + public: + GetElementCountTest() {} +}; + +TEST_F(GetElementCountTest, GetElementCount) +{ + std::vector shape; + int64_t expected_cnt; + + // Test 1: empty shape + shape = {}; + expected_cnt = 0; + assert_get_element_count_success(shape, expected_cnt); + + // Test 2: single dim + shape = {8}; + expected_cnt = 8; + assert_get_element_count_success(shape, expected_cnt); + + // Test 3: multiple dims + shape = {1, 2, 3, 4}; + expected_cnt = 24; + assert_get_element_count_success(shape, expected_cnt); +} + +TEST_F(GetElementCountTest, GetElementCountNegative) +{ + std::vector shape; + int64_t expected_cnt = -1; + + // Test 1: -1 dim + shape = {-1}; + assert_get_element_count_success(shape, expected_cnt); + + // Test 2: one -1 dim + shape = {-1, 8, 8}; + assert_get_element_count_success(shape, expected_cnt); + + // Test 3: multiple -1 dims + shape = {8, -1, -1}; + assert_get_element_count_success(shape, expected_cnt); + + // Test 4: -1 dim before overflow + shape = {-1, 1LL << 32, 1LL << 31}; + assert_get_element_count_success(shape, expected_cnt); +} + +TEST_F(GetElementCountTest, GetElementCountZero) +{ + std::vector shape; + int64_t expected_cnt = 0; + + // Test 1: 0 dim + shape = {0}; + assert_get_element_count_success(shape, expected_cnt); + + // Test 2: one 0 dim + shape = {1, 8, 0}; + assert_get_element_count_success(shape, expected_cnt); + + // Test 2: one 0 dim + shape = {0, 1, 8}; + assert_get_element_count_success(shape, expected_cnt); + + // Test 3: multiple 0 dims + shape = {8, 0, 0}; + assert_get_element_count_success(shape, expected_cnt); +} + +TEST_F(GetElementCountTest, GetElementCountInvalidDim) +{ + std::vector shape; + std::string error_msg; + + // Test 1: single invalid dim + shape = {1, -2}; + error_msg = std::string("shape") + tb::ShapeToString(shape) + + " contains an invalid dim."; + assert_get_element_count_error(shape, ErrorCode::kInvalidDim, error_msg); + + // Test 2: multiple invalid dims + shape = {1, -2, -3}; + error_msg = std::string("shape") + tb::ShapeToString(shape) + + " contains an invalid dim."; + assert_get_element_count_error(shape, ErrorCode::kInvalidDim, error_msg); + + // Test 3: valid but overflow dim + shape = {1, 1LL << 63}; + error_msg = std::string("shape") + tb::ShapeToString(shape) + + " contains an invalid dim."; + assert_get_element_count_error(shape, ErrorCode::kInvalidDim, error_msg); +} + +TEST_F(GetElementCountTest, GetElementCountOverflow) +{ + std::vector shape; + std::string error_msg; + + // Test 1: no overflow + shape = {1LL << 31, 1LL << 31}; + int64_t expected_cnt = 1LL << 62; + assert_get_element_count_success(shape, expected_cnt); + + // Test 2: overflows + shape = {1LL << 32, 1LL << 31}; + error_msg = "unexpected integer overflow while calculating element count."; + assert_get_element_count_error(shape, ErrorCode::kOverflow, error_msg); + + // Test 3: overflows before -1 dim + shape = {1LL << 32, 1LL << 31, -1}; + error_msg = "unexpected integer overflow while calculating element count."; + assert_get_element_count_error(shape, ErrorCode::kOverflow, error_msg); +} + +class GetByteSizeTest : public ::testing::Test { + public: + GetByteSizeTest() {} +}; + +TEST_F(GetByteSizeTest, GetByteSize) +{ + TRITONSERVER_DataType dtype = TRITONSERVER_TYPE_INT32; + std::vector shape; + int64_t expected_size; + + // Test 1: empty shape + shape = {}; + expected_size = 0; + assert_get_byte_size_success(dtype, shape, expected_size); + + // Test 2: single dim + shape = {8}; + expected_size = 8 * TRITONSERVER_DataTypeByteSize(dtype); + assert_get_byte_size_success(dtype, shape, expected_size); + + // Test 3: multiple dims + shape = {1, 2, 3, 4}; + expected_size = 24 * TRITONSERVER_DataTypeByteSize(dtype); + assert_get_byte_size_success(dtype, shape, expected_size); + + // Test 4: multiple dims with 0 + shape = {0, 1, 8}; + expected_size = 0; + assert_get_byte_size_success(dtype, shape, expected_size); +} + +TEST_F(GetByteSizeTest, GetByteSizeNegative) +{ + TRITONSERVER_DataType dtype; + std::vector shape; + int64_t expected_size = -1; + + // Test 1: invalid dtype + dtype = TRITONSERVER_TYPE_INVALID; + shape = {8, 8}; + assert_get_byte_size_success(dtype, shape, expected_size); + + // Test 2: bytes dtype + dtype = TRITONSERVER_TYPE_BYTES; + shape = {8, 8}; + assert_get_byte_size_success(dtype, shape, expected_size); + + // Test 3: invalid shape and element count overflows + dtype = TRITONSERVER_TYPE_INVALID; + shape = {1LL << 40, 1LL << 40}; + assert_get_byte_size_success(dtype, shape, expected_size); + + // Test 4: negative shape + dtype = TRITONSERVER_TYPE_INT32; + shape = {-1, 8}; + assert_get_byte_size_success(dtype, shape, expected_size); +} + +TEST_F(GetByteSizeTest, GetByteSizeZero) +{ + TRITONSERVER_DataType dtype = TRITONSERVER_TYPE_INT32; + std::vector shape; + int64_t expected_cnt = 0; + + // Test 1: 0 dim + shape = {0}; + assert_get_byte_size_success(dtype, shape, expected_cnt); + + // Test 2: one 0 dim + shape = {1, 8, 0}; + assert_get_byte_size_success(dtype, shape, expected_cnt); + + // Test 2: one 0 dim + shape = {0, 1, 8}; + assert_get_byte_size_success(dtype, shape, expected_cnt); + + // Test 3: multiple 0 dims + shape = {8, 0, 0}; + assert_get_byte_size_success(dtype, shape, expected_cnt); +} + +TEST_F(GetByteSizeTest, GetByteSizeInvalidDim) +{ + TRITONSERVER_DataType dtype = TRITONSERVER_TYPE_INT32; + std::vector shape; + std::string error_msg; + + // Test 1: single invalid dim + shape = {1, -2}; + error_msg = std::string("shape") + tb::ShapeToString(shape) + + " contains an invalid dim."; + assert_get_byte_size_error(dtype, shape, ErrorCode::kInvalidDim, error_msg); + + // Test 2: multiple invalid dims + shape = {1, -2, -3}; + error_msg = std::string("shape") + tb::ShapeToString(shape) + + " contains an invalid dim."; + assert_get_byte_size_error(dtype, shape, ErrorCode::kInvalidDim, error_msg); + + // Test 3: valid but overflow dim + shape = {1, 1LL << 63}; + error_msg = std::string("shape") + tb::ShapeToString(shape) + + " contains an invalid dim."; + assert_get_byte_size_error(dtype, shape, ErrorCode::kInvalidDim, error_msg); +} + +TEST_F(GetByteSizeTest, GetByteSizeOverflow) +{ + TRITONSERVER_DataType dtype = TRITONSERVER_TYPE_INT32; + std::vector shape; + std::string error_msg; + + // Test 1: no overflow + shape = {1LL << 30, 1LL << 30}; + int64_t expected_size = (1LL << 60) * TRITONSERVER_DataTypeByteSize(dtype); + assert_get_byte_size_success(dtype, shape, expected_size); + + // Test 2: element count overflows + shape = {1LL << 32, 1LL << 31}; + error_msg = "unexpected integer overflow while calculating byte size."; + assert_get_byte_size_error(dtype, shape, ErrorCode::kOverflow, error_msg); + + // Test 3: valid element count but byte size overflows + shape = {1LL << 31, 1LL << 30}; + error_msg = "unexpected integer overflow while calculating byte size."; + assert_get_byte_size_error(dtype, shape, ErrorCode::kOverflow, error_msg); +} +} // namespace + +int +main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From 4fc0a5ed464f201afa67ff8d0803bf6eaa72982a Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Wed, 7 May 2025 12:05:14 -0700 Subject: [PATCH 030/230] Update default branch post-25.04 (#8188) Co-authored-by: Dmitry Tokarev Co-authored-by: richardhuo-nv --- Dockerfile.sdk | 2 +- README.md | 4 ++-- TRITON_VERSION | 2 +- build.py | 8 ++++---- deploy/aws/values.yaml | 2 +- deploy/fleetcommand/Chart.yaml | 2 +- deploy/fleetcommand/values.yaml | 6 +++--- deploy/gcp/values.yaml | 2 +- .../benchmark/perf-analyzer-script/triton_client.yaml | 2 +- .../gke-marketplace-app/server-deployer/build_and_push.sh | 6 +++--- .../server-deployer/chart/triton/Chart.yaml | 4 ++-- .../server-deployer/chart/triton/values.yaml | 6 +++--- .../server-deployer/data-test/schema.yaml | 2 +- deploy/gke-marketplace-app/server-deployer/schema.yaml | 4 ++-- deploy/gke-marketplace-app/trt-engine/README.md | 6 +++--- deploy/k8s-onprem/values.yaml | 2 +- deploy/oci/values.yaml | 2 +- docs/introduction/release_notes.md | 4 ++-- python/openai/README.md | 2 +- qa/common/gen_jetson_trt_models | 2 +- qa/common/gen_qa_custom_ops | 2 +- qa/common/gen_qa_model_repository | 2 +- src/python/tritonfrontend/_api/_kservegrpc.py | 1 + src/python/tritonfrontend/_api/_kservegrpc.pyi | 1 + 24 files changed, 39 insertions(+), 37 deletions(-) diff --git a/Dockerfile.sdk b/Dockerfile.sdk index 15b164b20e..65d46a0f4d 100644 --- a/Dockerfile.sdk +++ b/Dockerfile.sdk @@ -29,7 +29,7 @@ # # Base image on the minimum Triton container -ARG BASE_IMAGE=nvcr.io/nvidia/tritonserver:25.03-py3-min +ARG BASE_IMAGE=nvcr.io/nvidia/tritonserver:25.04-py3-min ARG TRITON_CLIENT_REPO_SUBDIR=clientrepo ARG TRITON_PA_REPO_SUBDIR=perfanalyzerrepo diff --git a/README.md b/README.md index 63c67db1a1..78b50aff5f 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ >[!WARNING] >You are currently on the `main` branch which tracks under-development progress ->towards the next release. The current release is version [2.56.0](https://github.com/triton-inference-server/server/releases/latest) ->and corresponds to the 25.03 container release on NVIDIA GPU Cloud (NGC). +>towards the next release. The current release is version [2.57.0](https://github.com/triton-inference-server/server/releases/latest) +>and corresponds to the 25.04 container release on NVIDIA GPU Cloud (NGC). # Triton Inference Server diff --git a/TRITON_VERSION b/TRITON_VERSION index a6bb59206c..7bf8e9ee0c 100644 --- a/TRITON_VERSION +++ b/TRITON_VERSION @@ -1 +1 @@ -2.57.0dev +2.58.0dev diff --git a/build.py b/build.py index 61f5a94121..c3bd4bb917 100755 --- a/build.py +++ b/build.py @@ -71,14 +71,14 @@ # DEFAULT_TRITON_VERSION_MAP = { - "release_version": "2.57.0dev", - "triton_container_version": "25.04dev", - "upstream_container_version": "25.03", + "release_version": "2.58.0dev", + "triton_container_version": "25.05dev", + "upstream_container_version": "25.04", "ort_version": "1.21.0", "ort_openvino_version": "2025.0.0", "standalone_openvino_version": "2025.0.0", "dcgm_version": "3.3.6", - "vllm_version": "0.7.3", + "vllm_version": "0.8.1", "rhel_py_version": "3.12.3", } diff --git a/deploy/aws/values.yaml b/deploy/aws/values.yaml index d7b424435c..1e0f283b1d 100644 --- a/deploy/aws/values.yaml +++ b/deploy/aws/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.03-py3 + imageName: nvcr.io/nvidia/tritonserver:25.04-py3 pullPolicy: IfNotPresent modelRepositoryPath: s3://triton-inference-server-repository/model_repository numGpus: 1 diff --git a/deploy/fleetcommand/Chart.yaml b/deploy/fleetcommand/Chart.yaml index 481c4bd44c..fe6125a34e 100644 --- a/deploy/fleetcommand/Chart.yaml +++ b/deploy/fleetcommand/Chart.yaml @@ -26,7 +26,7 @@ apiVersion: v1 # appVersion is the Triton version; update when changing release -appVersion: "2.56.0" +appVersion: "2.57.0" description: Triton Inference Server (Fleet Command) name: triton-inference-server # version is the Chart version; update when changing anything in the chart diff --git a/deploy/fleetcommand/values.yaml b/deploy/fleetcommand/values.yaml index a8458c97dc..e149a48561 100644 --- a/deploy/fleetcommand/values.yaml +++ b/deploy/fleetcommand/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.03-py3 + imageName: nvcr.io/nvidia/tritonserver:25.04-py3 pullPolicy: IfNotPresent numGpus: 1 serverCommand: tritonserver @@ -47,13 +47,13 @@ image: # # To set model control mode, uncomment and configure below # TODO: Fix the following url, it is invalid - # See https://github.com/triton-inference-server/server/blob/r25.03/docs/model_management.md + # See https://github.com/triton-inference-server/server/blob/r25.04/docs/user_guide/model_management.md # for more details #- --model-control-mode=explicit|poll|none # # Additional server args # - # see https://github.com/triton-inference-server/server/blob/r25.03/README.md + # see https://github.com/triton-inference-server/server/blob/r25.04/README.md # for more details service: diff --git a/deploy/gcp/values.yaml b/deploy/gcp/values.yaml index eb0a18b32f..3b6d07e771 100644 --- a/deploy/gcp/values.yaml +++ b/deploy/gcp/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.03-py3 + imageName: nvcr.io/nvidia/tritonserver:25.04-py3 pullPolicy: IfNotPresent modelRepositoryPath: gs://triton-inference-server-repository/model_repository numGpus: 1 diff --git a/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml b/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml index 1671b334c8..b3c136898c 100644 --- a/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml +++ b/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml @@ -33,7 +33,7 @@ metadata: namespace: default spec: containers: - - image: nvcr.io/nvidia/tritonserver:25.03-py3-sdk + - image: nvcr.io/nvidia/tritonserver:25.04-py3-sdk imagePullPolicy: Always name: nv-triton-client securityContext: diff --git a/deploy/gke-marketplace-app/server-deployer/build_and_push.sh b/deploy/gke-marketplace-app/server-deployer/build_and_push.sh index 31af7064f5..237b70b179 100755 --- a/deploy/gke-marketplace-app/server-deployer/build_and_push.sh +++ b/deploy/gke-marketplace-app/server-deployer/build_and_push.sh @@ -27,9 +27,9 @@ export REGISTRY=gcr.io/$(gcloud config get-value project | tr ':' '/') export APP_NAME=tritonserver -export MAJOR_VERSION=2.56 -export MINOR_VERSION=2.56.0 -export NGC_VERSION=25.03-py3 +export MAJOR_VERSION=2.57 +export MINOR_VERSION=2.57.0 +export NGC_VERSION=25.04-py3 docker pull nvcr.io/nvidia/$APP_NAME:$NGC_VERSION diff --git a/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml b/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml index 1d490f7c9c..82cd327fd4 100644 --- a/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml +++ b/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml @@ -25,7 +25,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. apiVersion: v1 -appVersion: "2.56" +appVersion: "2.57" description: Triton Inference Server name: triton-inference-server -version: 2.56.0 +version: 2.57.0 diff --git a/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml b/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml index d97d89f04d..f6a61d6f6c 100644 --- a/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml +++ b/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml @@ -31,14 +31,14 @@ maxReplicaCount: 3 tritonProtocol: HTTP # HPA GPU utilization autoscaling target HPATargetAverageValue: 85 -modelRepositoryPath: gs://triton_sample_models/25.03 -publishedVersion: '2.56.0' +modelRepositoryPath: gs://triton_sample_models/25.04 +publishedVersion: '2.57.0' gcpMarketplace: true image: registry: gcr.io repository: nvidia-ngc-public/tritonserver - tag: 25.03-py3 + tag: 25.04-py3 pullPolicy: IfNotPresent # modify the model repository here to match your GCP storage bucket numGpus: 1 diff --git a/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml b/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml index 7c5f5cc425..79ee70d01e 100644 --- a/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml +++ b/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml @@ -27,7 +27,7 @@ x-google-marketplace: schemaVersion: v2 applicationApiVersion: v1beta1 - publishedVersion: '2.56.0' + publishedVersion: '2.57.0' publishedVersionMetadata: releaseNote: >- Initial release. diff --git a/deploy/gke-marketplace-app/server-deployer/schema.yaml b/deploy/gke-marketplace-app/server-deployer/schema.yaml index 2569628502..1cff4c3c40 100644 --- a/deploy/gke-marketplace-app/server-deployer/schema.yaml +++ b/deploy/gke-marketplace-app/server-deployer/schema.yaml @@ -27,7 +27,7 @@ x-google-marketplace: schemaVersion: v2 applicationApiVersion: v1beta1 - publishedVersion: '2.56.0' + publishedVersion: '2.57.0' publishedVersionMetadata: releaseNote: >- Initial release. @@ -89,7 +89,7 @@ properties: modelRepositoryPath: type: string title: Bucket where models are stored. Please make sure the user/service account to create the GKE app has permission to this GCS bucket. Read Triton documentation on configs and formatting details, supporting TensorRT, TensorFlow, Pytorch, Onnx ... etc. - default: gs://triton_sample_models/25.03 + default: gs://triton_sample_models/25.04 image.ldPreloadPath: type: string title: Leave this empty by default. Triton allows users to create custom layers for backend such as TensorRT plugin or Tensorflow custom ops, the compiled shared library must be provided via LD_PRELOAD environment variable. diff --git a/deploy/gke-marketplace-app/trt-engine/README.md b/deploy/gke-marketplace-app/trt-engine/README.md index 8b3e284fdd..ff7e5cdaef 100644 --- a/deploy/gke-marketplace-app/trt-engine/README.md +++ b/deploy/gke-marketplace-app/trt-engine/README.md @@ -33,7 +33,7 @@ ``` docker run --gpus all -it --network host \ --shm-size=1g --ulimit memlock=-1 --ulimit stack=67108864 \ - -v ~:/scripts nvcr.io/nvidia/tensorrt:25.03-py3 + -v ~:/scripts nvcr.io/nvidia/tensorrt:25.04-py3 pip install onnx six torch tf2onnx tensorflow @@ -57,7 +57,7 @@ mkdir -p engines python3 builder.py -m models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/model.ckpt -o engines/bert_large_int8_bs1_s128.engine -b 1 -s 128 -c models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/ -v models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/vocab.txt --int8 --fp16 --strict --calib-num 1 -iln -imh -gsutil cp bert_large_int8_bs1_s128.engine gs://triton_sample_models/25.03/bert/1/model.plan +gsutil cp bert_large_int8_bs1_s128.engine gs://triton_sample_models/25.04/bert/1/model.plan ``` -For each Triton upgrade, container version used to generate the model, and the model path in GCS `gs://triton_sample_models/25.03/` should be updated accordingly with the correct version. +For each Triton upgrade, container version used to generate the model, and the model path in GCS `gs://triton_sample_models/25.04/` should be updated accordingly with the correct version. diff --git a/deploy/k8s-onprem/values.yaml b/deploy/k8s-onprem/values.yaml index aa1a7e9aee..442288b5a7 100644 --- a/deploy/k8s-onprem/values.yaml +++ b/deploy/k8s-onprem/values.yaml @@ -30,7 +30,7 @@ tags: openshift: false image: - imageName: nvcr.io/nvidia/tritonserver:25.03-py3 + imageName: nvcr.io/nvidia/tritonserver:25.04-py3 pullPolicy: IfNotPresent modelRepositoryServer: < Replace with the IP Address of your file server > modelRepositoryPath: /srv/models diff --git a/deploy/oci/values.yaml b/deploy/oci/values.yaml index 6c51c8ee90..88be9847d3 100644 --- a/deploy/oci/values.yaml +++ b/deploy/oci/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.03-py3 + imageName: nvcr.io/nvidia/tritonserver:25.04-py3 pullPolicy: IfNotPresent modelRepositoryPath: s3://https://.compat.objectstorage..oraclecloud.com:443/triton-inference-server-repository numGpus: 1 diff --git a/docs/introduction/release_notes.md b/docs/introduction/release_notes.md index d0516e23c3..105cf44ca7 100644 --- a/docs/introduction/release_notes.md +++ b/docs/introduction/release_notes.md @@ -25,9 +25,9 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --> -# [Triton Inference Server Release 25.03](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-25-03.html#rel-25-03) +# [Triton Inference Server Release 25.04](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-25-04.html#rel-25-04) -The Triton Inference Server container image, release 25.03, is available +The Triton Inference Server container image, release 25.04, is available on [NGC](https://ngc.nvidia.com/catalog/containers/nvidia:tritonserver) and is open source on [GitHub](https://github.com/triton-inference-server/server). Release notes can diff --git a/python/openai/README.md b/python/openai/README.md index 4e6f05b44a..08d1780c84 100644 --- a/python/openai/README.md +++ b/python/openai/README.md @@ -51,7 +51,7 @@ docker run -it --net=host --gpus all --rm \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ -e HF_TOKEN \ - nvcr.io/nvidia/tritonserver:25.03-vllm-python-py3 + nvcr.io/nvidia/tritonserver:25.04-vllm-python-py3 ``` 2. Launch the OpenAI-compatible Triton Inference Server: diff --git a/qa/common/gen_jetson_trt_models b/qa/common/gen_jetson_trt_models index 1dda5b72cf..f9d56c2815 100755 --- a/qa/common/gen_jetson_trt_models +++ b/qa/common/gen_jetson_trt_models @@ -34,7 +34,7 @@ # Make all generated files accessible outside of container umask 0000 # Set the version of the models -TRITON_VERSION=${TRITON_VERSION:=25.03} +TRITON_VERSION=${TRITON_VERSION:=25.04} # Set the CUDA device to use CUDA_DEVICE=${RUNNER_ID:=0} # Set TensorRT image diff --git a/qa/common/gen_qa_custom_ops b/qa/common/gen_qa_custom_ops index 304e126c34..e12cd89960 100755 --- a/qa/common/gen_qa_custom_ops +++ b/qa/common/gen_qa_custom_ops @@ -37,7 +37,7 @@ ## ############################################################################ -TRITON_VERSION=${TRITON_VERSION:=25.03} +TRITON_VERSION=${TRITON_VERSION:=25.04} NVIDIA_UPSTREAM_VERSION=${NVIDIA_UPSTREAM_VERSION:=$TRITON_VERSION} PYTORCH_IMAGE=${PYTORCH_IMAGE:=nvcr.io/nvidia/pytorch:$NVIDIA_UPSTREAM_VERSION-py3} UBUNTU_IMAGE=${UBUNTU_IMAGE:=ubuntu:24.04} diff --git a/qa/common/gen_qa_model_repository b/qa/common/gen_qa_model_repository index 0b5e0e9e1d..6d3db26edc 100755 --- a/qa/common/gen_qa_model_repository +++ b/qa/common/gen_qa_model_repository @@ -48,7 +48,7 @@ ## ############################################################################ -TRITON_VERSION=${TRITON_VERSION:=25.03} +TRITON_VERSION=${TRITON_VERSION:=25.04} # ONNX. Use ONNX_OPSET 0 to use the default for ONNX version ONNX_VERSION=1.16.1 diff --git a/src/python/tritonfrontend/_api/_kservegrpc.py b/src/python/tritonfrontend/_api/_kservegrpc.py index 54959e1c89..ef5fa9e35f 100644 --- a/src/python/tritonfrontend/_api/_kservegrpc.py +++ b/src/python/tritonfrontend/_api/_kservegrpc.py @@ -79,6 +79,7 @@ class Options: infer_compression_level: Union[ int, Grpc_compression_level ] = Grpc_compression_level.NONE + infer_thread_count: int = Field(2, ge=0) infer_allocation_pool_size: int = Field(8, ge=0) max_response_pool_size: int = Field(2_147_483_647, ge=0) forward_header_pattern: str = "" diff --git a/src/python/tritonfrontend/_api/_kservegrpc.pyi b/src/python/tritonfrontend/_api/_kservegrpc.pyi index 0c8dc04804..cacfe24e56 100644 --- a/src/python/tritonfrontend/_api/_kservegrpc.pyi +++ b/src/python/tritonfrontend/_api/_kservegrpc.pyi @@ -56,6 +56,7 @@ class KServeGrpc: max_connection_age_ms: int max_connection_age_grace_ms: int infer_compression_level: int | Grpc_compression_level + infer_thread_count: int infer_allocation_pool_size: int max_response_pool_size: int forward_header_pattern: str From d6750e89394d4ad3b46e0f28c2bae85ae52db0ff Mon Sep 17 00:00:00 2001 From: mattwittwer Date: Wed, 7 May 2025 13:34:40 -0700 Subject: [PATCH 031/230] Fix: Update element count handling (#8182) Ensures requests with overly large or invalid element counts are rejected with a relevant error message. --- qa/L0_http/http_test.py | 98 +++++++++++++++++++++++++++++++++++++++++ qa/L0_http/test.sh | 2 +- src/common.cc | 35 ++++++++++++++- src/common.h | 11 ++++- src/http_server.cc | 38 ++++++++++++++-- 5 files changed, 177 insertions(+), 7 deletions(-) diff --git a/qa/L0_http/http_test.py b/qa/L0_http/http_test.py index a838283e0d..6786545f2f 100755 --- a/qa/L0_http/http_test.py +++ b/qa/L0_http/http_test.py @@ -236,6 +236,104 @@ def test_descriptive_status_code(self): ) t.join() + def test_buffer_size_overflow(self): + model = "onnx_zero_1_float32" + + # Test for overflow within GetElementCount() + payload1 = { + "inputs": [ + { + "name": "INPUT0", + "shape": [ + 2**4, + 2**60 + 2, + ], # This evaluates to 2^64 + 32 during GetElementCount() + "datatype": "FP32", + "data": [1.0], + } + ] + } + + # Test for overflow with type_byte_size multiplication + payload2 = { + "inputs": [ + { + "name": "INPUT0", + "shape": [ + 2**2, + 2**60 + 2, + ], # This evaluates to 2^64 + 32 during type_byte_size multiplication since FP32 is 4 bytes + "datatype": "FP32", + "data": [1.0], + } + ] + } + + # Send request and expect a 400 error with specific overflow message + headers = {"Content-Type": "application/json"} + + # Test the first payload (GetElementCount overflow) + r1 = requests.post(self._get_infer_url(model), json=payload1, headers=headers) + + self.assertEqual( + 400, + r1.status_code, + "Expected error code 400 for GetElementCount overflow check; got: {}".format( + r1.status_code + ), + ) + + error_message1 = r1.content.decode() + self.assertIn( + "causes total element count to exceed maximum size of", error_message1 + ) + + # Test the second payload (type_byte_size multiplication overflow) + r2 = requests.post(self._get_infer_url(model), json=payload2, headers=headers) + + self.assertEqual( + 400, + r2.status_code, + "Expected error code 400 for type_byte_size multiplication overflow check; got: {}".format( + r2.status_code + ), + ) + + error_message2 = r2.content.decode() + self.assertIn("byte size overflow for input", error_message2) + + def test_negative_dimensions(self): + model = "onnx_zero_1_float32" + + payload = { + "inputs": [ + { + "name": "INPUT0", + "shape": [2, -5], # Negative dimension should be invalid + "datatype": "FP32", + "data": [1.0], + } + ] + } + + # Send request and expect a 500 error + headers = {"Content-Type": "application/json"} + r = requests.post(self._get_infer_url(model), json=payload, headers=headers) + + self.assertEqual( + 500, + r.status_code, + "Expected error code 500 for negative dimension; got: {}".format( + r.status_code + ), + ) + + error_message = r.content.decode() + self.assertIn( + "Unable to parse 'shape': attempt to access JSON non-unsigned-integer as unsigned-integer", + error_message, + ) + def test_loading_large_invalid_model(self): # Generate large base64 encoded data data_length = 1 << 31 diff --git a/qa/L0_http/test.sh b/qa/L0_http/test.sh index 1146523036..3c3df56e65 100755 --- a/qa/L0_http/test.sh +++ b/qa/L0_http/test.sh @@ -630,7 +630,7 @@ fi TEST_RESULT_FILE='test_results.txt' PYTHON_TEST=http_test.py -EXPECTED_NUM_TESTS=11 +EXPECTED_NUM_TESTS=13 set +e python $PYTHON_TEST >$CLIENT_LOG 2>&1 if [ $? -ne 0 ]; then diff --git a/src/common.cc b/src/common.cc index bf697e6752..90725041d1 100644 --- a/src/common.cc +++ b/src/common.cc @@ -1,4 +1,4 @@ -// Copyright 2020-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -80,6 +80,31 @@ GetEnvironmentVariableOrDefault( return value ? value : default_value; } +std::string +ShapeToString(const int64_t* dims, const size_t dims_count) +{ + bool first = true; + + std::string str("["); + for (size_t i = 0; i < dims_count; ++i) { + const int64_t dim = dims[i]; + if (!first) { + str += ","; + } + str += std::to_string(dim); + first = false; + } + + str += "]"; + return str; +} + +std::string +ShapeToString(const std::vector& shape) +{ + return ShapeToString(shape.data(), shape.size()); +} + int64_t GetElementCount(const std::vector& dims) { @@ -88,12 +113,20 @@ GetElementCount(const std::vector& dims) for (auto dim : dims) { if (dim == WILDCARD_DIM) { return -1; + } else if (dim < 0) { // invalid dim + return -2; + } else if (dim == 0) { + return 0; } if (first) { cnt = dim; first = false; } else { + // Check for overflow before multiplication + if (cnt > (INT64_MAX / dim)) { + return -3; + } cnt *= dim; } } diff --git a/src/common.h b/src/common.h index 02e9e1a890..433b3bbf78 100644 --- a/src/common.h +++ b/src/common.h @@ -158,11 +158,18 @@ std::string GetEnvironmentVariableOrDefault( /// Get the number of elements in a shape. /// /// \param dims The shape. -/// \return The number of elements, or -1 if the number of elements +/// \return The number of elements, -1 if the number of elements /// cannot be determined because the shape contains one or more -/// wildcard dimensions. +/// wildcard dimensions, -2 if the shape contains an invalid dim, +/// or -3 if the number is too large to represent as an int64_t. int64_t GetElementCount(const std::vector& dims); +/// Convert shape to string representation. +/// +/// \param shape The shape as a vector. +/// \return The string representation of the shape. +std::string ShapeToString(const std::vector& shape); + /// Returns if 'vec' contains 'str'. /// /// \param vec The vector of strings to search. diff --git a/src/http_server.cc b/src/http_server.cc index b08a1f5ccb..8c2437d99c 100644 --- a/src/http_server.cc +++ b/src/http_server.cc @@ -2755,12 +2755,29 @@ HTTPAPIServer::ParseJsonTritonIO( } else { const int64_t element_cnt = GetElementCount(shape_vec); - // FIXME, element count should never be 0 or negative so - // shouldn't we just return an error here? if (element_cnt == 0) { RETURN_IF_ERR(TRITONSERVER_InferenceRequestAppendInputData( irequest, input_name, nullptr, 0 /* byte_size */, TRITONSERVER_MEMORY_CPU, 0 /* memory_type_id */)); + } else if (element_cnt == -2) { + // -2 indicates invalid dimension + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + std::string( + "invalid shape for input '" + std::string(input_name) + + "': shape " + ShapeToString(shape_vec) + + " contains one or more invalid dimensions") + .c_str()); + } else if (element_cnt == -3) { + // -3 indicates integer overflow + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + std::string( + "invalid shape for input '" + std::string(input_name) + + "': shape " + ShapeToString(shape_vec) + + " causes total element count to exceed maximum size of " + + std::to_string(INT64_MAX)) + .c_str()); } else { // JSON... presence of "data" already validated but still // checking here. Flow in this endpoint needs to be @@ -2773,7 +2790,22 @@ HTTPAPIServer::ParseJsonTritonIO( if (dtype == TRITONSERVER_TYPE_BYTES) { RETURN_IF_ERR(JsonBytesArrayByteSize(tensor_data, &byte_size)); } else { - byte_size = element_cnt * TRITONSERVER_DataTypeByteSize(dtype); + const uint32_t type_byte_size = + TRITONSERVER_DataTypeByteSize(dtype); + if ((type_byte_size > 1) && + (element_cnt > (INT64_MAX / type_byte_size))) { + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + std::string( + "byte size overflow for input '" + + std::string(input_name) + "': element count (" + + std::to_string(element_cnt) + ") * data type size (" + + std::to_string(type_byte_size) + + ") exceeds maximum allowed size (" + + std::to_string(INT64_MAX) + ")") + .c_str()); + } + byte_size = element_cnt * type_byte_size; } infer_req->serialized_data_.emplace_back(); From cb8fe02ee9b19be403a5c507e0f478d83ea60698 Mon Sep 17 00:00:00 2001 From: Olga Andreeva <124622579+oandreeva-nv@users.noreply.github.com> Date: Thu, 8 May 2025 10:08:35 -0700 Subject: [PATCH 032/230] [build] boost vLLM to 0.8.5 (#8193) --- build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.py b/build.py index c3bd4bb917..27103d27cd 100755 --- a/build.py +++ b/build.py @@ -78,7 +78,7 @@ "ort_openvino_version": "2025.0.0", "standalone_openvino_version": "2025.0.0", "dcgm_version": "3.3.6", - "vllm_version": "0.8.1", + "vllm_version": "0.8.5", "rhel_py_version": "3.12.3", } From 578096b8f31c45471e90c3de6d0eba550b8e389a Mon Sep 17 00:00:00 2001 From: Misha Chornyi Date: Thu, 8 May 2025 11:01:57 -0700 Subject: [PATCH 033/230] TPRD-1508: Update ONNX Runtime version to 1.22.0 --- build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.py b/build.py index 27103d27cd..1509b30631 100755 --- a/build.py +++ b/build.py @@ -74,7 +74,7 @@ "release_version": "2.58.0dev", "triton_container_version": "25.05dev", "upstream_container_version": "25.04", - "ort_version": "1.21.0", + "ort_version": "1.22.0", "ort_openvino_version": "2025.0.0", "standalone_openvino_version": "2025.0.0", "dcgm_version": "3.3.6", From f709c33b6d4d7d81828e486ce57bbbac737d88b3 Mon Sep 17 00:00:00 2001 From: Misha Chornyi Date: Thu, 8 May 2025 12:30:02 -0700 Subject: [PATCH 034/230] TPRD-1509: Uppdate OpenVINO version to 2025.1.0 --- build.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.py b/build.py index 1509b30631..72dbf0980b 100755 --- a/build.py +++ b/build.py @@ -75,8 +75,8 @@ "triton_container_version": "25.05dev", "upstream_container_version": "25.04", "ort_version": "1.22.0", - "ort_openvino_version": "2025.0.0", - "standalone_openvino_version": "2025.0.0", + "ort_openvino_version": "2025.1.0", + "standalone_openvino_version": "2025.1.0", "dcgm_version": "3.3.6", "vllm_version": "0.8.5", "rhel_py_version": "3.12.3", From 1b398439b305799f6a951868f4fc9ab94239a9ae Mon Sep 17 00:00:00 2001 From: Olga Andreeva <124622579+oandreeva-nv@users.noreply.github.com> Date: Fri, 9 May 2025 10:03:43 -0700 Subject: [PATCH 035/230] proper 25.05 version (#8198) --- build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.py b/build.py index 72dbf0980b..e174c9b281 100755 --- a/build.py +++ b/build.py @@ -78,7 +78,7 @@ "ort_openvino_version": "2025.1.0", "standalone_openvino_version": "2025.1.0", "dcgm_version": "3.3.6", - "vllm_version": "0.8.5", + "vllm_version": "0.8.4", "rhel_py_version": "3.12.3", } From 7866ef4e7b12bc6239d06ee5a43e75ed8cbf96c9 Mon Sep 17 00:00:00 2001 From: Dmitry Tokarev Date: Wed, 14 May 2025 15:39:03 -0400 Subject: [PATCH 036/230] R25.04 compatibility.md updates (#8201) (#8206) Co-authored-by: Kyle McGill <101670481+nv-kmcgill53@users.noreply.github.com> --- docs/introduction/compatibility.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/introduction/compatibility.md b/docs/introduction/compatibility.md index be6e1da66f..1fafebd88b 100644 --- a/docs/introduction/compatibility.md +++ b/docs/introduction/compatibility.md @@ -29,14 +29,17 @@ [Please visit Deep Learning Framework (DLFW) website for the complete compatibility matrix](https://docs.nvidia.com/deeplearning/frameworks/support-matrix/index.html). # Release Compatibility Matrix - 1. [Container Name: trtllm-python-py3](#container-name-trtllm-python-py3) - 2. [Container Name: vllm-python-py3](#container-name-vllm-python-py3) - 3. [ONNX Runtime Versions](#onnx-runtime-versions) +- [Release Compatibility Matrix](#release-compatibility-matrix) + - [Container Name: trtllm-python-py3](#container-name-trtllm-python-py3) + - [Container Name: vllm-python-py3](#container-name-vllm-python-py3) + - [ONNX Runtime Versions](#onnx-runtime-versions) ## Container Name: trtllm-python-py3 | Triton release version | NGC Tag | Python version | Torch version | TensorRT version | TensorRT-LLM version | CUDA version | CUDA Driver version | Size | | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 25.04 | nvcr.io/nvidia/tritonserver:25.04-trtllm-python-py3 | Python 3.12.3 | 2.7.0a0+7c8ec84dab.nv25.3 | 10.9.0.34 | 0.18.2 | 12.8.1.012 | 570.124.06 | 17G | +| 25.03 | nvcr.io/nvidia/tritonserver:25.03-trtllm-python-py3 | Python 3.12.3 | 2.7.0a0%2B7c8ec84dab.nv25.3 | 10.9.0.34 | 0.18.0 | 12.8.1.012 | 570.124.06 | 28G | | 25.02 | nvcr.io/nvidia/tritonserver:25.02-trtllm-python-py3 | Python 3.12.3 | 2.6.0a0%2Becf3bae40a.nv25.1 | 10.8.0.43 | 0.17.0.post1 | 12.8.0.038 | 570.86.10 | 28G | | 25.01 | nvcr.io/nvidia/tritonserver:25.01-trtllm-python-py3 | Python 3.12.3 | 2.6.0a0%2Becf3bae40a.nv25.1 | 10.8.0.43 | 0.17.0 | 12.8.0.038 | 570.86.10 | 30G | | 24.12 | nvcr.io/nvidia/tritonserver:24.12-trtllm-python-py3 | Python 3.12.3 | 2.6.0a0%2Bdf5bbc09d1.nv24.11 | 10.7.0 | 0.16.0 | 12.6.3 | 560.35.05 | 22G | @@ -53,6 +56,8 @@ | Triton release version | NGC Tag | Python version | vLLM version | CUDA version | CUDA Driver version | Size | | --- | --- | --- | --- | --- | --- | --- | +| 25.04 | nvcr.io/nvidia/tritonserver:25.04-vllm-python-py3 | Python 3.12.3 | 0.8.1+5f4af9e0.nv25.4.cu129 | 12.9.0.036 | 575.51.02 | 10G | +| 25.03 | nvcr.io/nvidia/tritonserver:25.03-vllm-python-py3 | Python 3.12.3 | 0.7.3+04de634a.nv25.3.cu128 | 12.8.1.012 | 570.124.06 | 22G | | 25.02 | nvcr.io/nvidia/tritonserver:25.02-vllm-python-py3 | Python 3.12.3 | 0.7.0+5e800e3d.nv25.2.cu128 | 12.8.0.038 | 570.86.10 | 22G | | 25.01 | nvcr.io/nvidia/tritonserver:25.01-vllm-python-py3 | Python 3.12.3 | 0.6.3.post1 | 12.8.0.038 | 570.86.10 | 23G | | 24.12 | nvcr.io/nvidia/tritonserver:24.12-vllm-python-py3 | Python 3.12.3 | 0.5.5 | 12.6.3.004 | 560.35.05 | 20G | @@ -69,6 +74,8 @@ | Triton release version | ONNX Runtime | | --- | --- | +| 25.04 | 1.21.0 | +| 25.03 | 1.21.0 | | 25.02 | 1.20.1 | | 25.01 | 1.20.1 | | 24.12 | 1.20.1 | From b8caffb290cf416c82ce18e49060a2f359f06752 Mon Sep 17 00:00:00 2001 From: Kris Hung Date: Mon, 19 May 2025 09:41:10 -0700 Subject: [PATCH 037/230] fix: Fix L0_backend_python test (#8178) --- qa/L0_backend_python/common.sh | 8 ++++++-- qa/L0_backend_python/env/test.sh | 18 ++++++------------ qa/L0_backend_python/examples/test.sh | 4 ++-- .../setup_python_enviroment.sh | 2 +- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/qa/L0_backend_python/common.sh b/qa/L0_backend_python/common.sh index 70fb3cc081..c6872cdf4c 100755 --- a/qa/L0_backend_python/common.sh +++ b/qa/L0_backend_python/common.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2021-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -85,7 +85,11 @@ create_conda_env_with_specified_path() { create_python_backend_stub() { rm -rf python_backend git clone ${TRITON_REPO_ORGANIZATION}/python_backend -b $PYTHON_BACKEND_REPO_TAG + CUDA_PATH=$(readlink -f /usr/local/cuda) (cd python_backend/ && mkdir builddir && cd builddir && \ - cmake -DTRITON_ENABLE_GPU=ON -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} -DTRITON_BACKEND_REPO_TAG=$TRITON_BACKEND_REPO_TAG -DTRITON_COMMON_REPO_TAG=$TRITON_COMMON_REPO_TAG -DTRITON_CORE_REPO_TAG=$TRITON_CORE_REPO_TAG -DPYBIND11_PYTHON_VERSION=$PY_VERSION ../ && \ + cmake -DTRITON_ENABLE_GPU=ON -DCMAKE_CUDA_COMPILER=$CUDA_PATH/bin/nvcc \ + -DCUDAToolkit_ROOT=$CUDA_PATH -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ + -DTRITON_BACKEND_REPO_TAG=$TRITON_BACKEND_REPO_TAG -DTRITON_COMMON_REPO_TAG=$TRITON_COMMON_REPO_TAG \ + -DTRITON_CORE_REPO_TAG=$TRITON_CORE_REPO_TAG -DPYBIND11_PYTHON_VERSION=$PY_VERSION ../ && \ make -j18 triton-python-backend-stub) } diff --git a/qa/L0_backend_python/env/test.sh b/qa/L0_backend_python/env/test.sh index 4d9de30a56..91870cbff3 100755 --- a/qa/L0_backend_python/env/test.sh +++ b/qa/L0_backend_python/env/test.sh @@ -53,8 +53,8 @@ conda install numpy=1.26.4 -y if [ $TRITON_RHEL -eq 1 ]; then TORCH_VERISON="2.17.0" fi -conda install torch=${TORCH_VERSION} -y -PY312_VERSION_STRING="Python version is 3.12, NumPy version is 1.26.4, and PyTorch version is ${TORCH_VERISON}" +conda install pytorch=${TORCH_VERSION} -y +PY312_VERSION_STRING="Python version is 3.12, NumPy version is 1.26.4, and PyTorch version is ${TORCH_VERSION}" conda pack -o python3.12.tar.gz mkdir -p models/python_3_12/1/ cp ../../python_models/python_version/config.pbtxt ./models/python_3_12 @@ -122,7 +122,7 @@ fi kill_server set +e -grep "Locale is ('en_US', 'UTF-8')" $SERVER_LOG +grep "Locale is ('C', 'UTF-8')" $SERVER_LOG if [ $? -ne 0 ]; then cat $SERVER_LOG echo -e "\n***\n*** Locale UTF-8 was not found in Triton logs. \n***" @@ -182,10 +182,6 @@ aws s3 mb "${BUCKET_URL}" BUCKET_URL=${BUCKET_URL%/} BUCKET_URL_SLASH="${BUCKET_URL}/" -# Remove Python 3.7 model because it contains absolute paths and cannot be used -# with S3. -rm -rf models/python_3_7 - # Test with the bucket url as model repository aws s3 cp models/ "${BUCKET_URL_SLASH}" --recursive --include "*" @@ -205,10 +201,10 @@ fi kill_server set +e -grep "$PY36_VERSION_STRING" $SERVER_LOG +grep "$PY312_VERSION_STRING" $SERVER_LOG if [ $? -ne 0 ]; then cat $SERVER_LOG - echo -e "\n***\n*** $PY36_VERSION_STRING was not found in Triton logs. \n***" + echo -e "\n***\n*** $PY312_VERSION_STRING was not found in Triton logs. \n***" RET=1 fi set -e @@ -217,8 +213,6 @@ set -e aws s3 rm "${BUCKET_URL_SLASH}" --recursive --include "*" # Test with EXECUTION_ENV_PATH outside the model directory -sed -i "s/TRITON_MODEL_DIRECTORY\/python_3_6_environment/TRITON_MODEL_DIRECTORY\/..\/python_3_6_environment/" models/python_3_6/config.pbtxt -mv models/python_3_6/python_3_6_environment.tar.gz models sed -i "s/\$\$TRITON_MODEL_DIRECTORY\/python_3_12_environment/s3:\/\/triton-bucket-${CI_JOB_ID}\/python_3_12_environment/" models/python_3_12/config.pbtxt mv models/python_3_12/python_3_12_environment.tar.gz models @@ -238,7 +232,7 @@ fi kill_server set +e -for EXPECTED_VERSION_STRING in "$PY36_VERSION_STRING" "$PY312_VERSION_STRING"; do +for EXPECTED_VERSION_STRING in "$PY312_VERSION_STRING"; do grep "$EXPECTED_VERSION_STRING" $SERVER_LOG if [ $? -ne 0 ]; then cat $SERVER_LOG diff --git a/qa/L0_backend_python/examples/test.sh b/qa/L0_backend_python/examples/test.sh index ea0fcd992c..c074b8e310 100755 --- a/qa/L0_backend_python/examples/test.sh +++ b/qa/L0_backend_python/examples/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2021-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -69,7 +69,7 @@ pip3 install validators # Install JAX # Jax has dropped the support for Python 3.8. See https://jax.readthedocs.io/en/latest/changelog.html if [ "$TEST_JETSON" == "0" ] && [ ${PYTHON_ENV_VERSION} != "8" ]; then - pip3 install --upgrade "jax[cuda12_local]" -f https://storage.googleapis.com/jax-releases/jax_cuda_releases.html + pip install -U "jax[cuda12]" fi git clone ${TRITON_REPO_ORGANIZATION}/python_backend -b $PYTHON_BACKEND_REPO_TAG diff --git a/qa/L0_backend_python/setup_python_enviroment.sh b/qa/L0_backend_python/setup_python_enviroment.sh index c3d0a0aed7..6e12f05dea 100755 --- a/qa/L0_backend_python/setup_python_enviroment.sh +++ b/qa/L0_backend_python/setup_python_enviroment.sh @@ -56,7 +56,7 @@ conda update -n base -c defaults conda -y # been setup correctly. if [ ${PYTHON_ENV_VERSION} = "11" ]; then create_conda_env "3.11" "python-3-11" - conda install torch=2.6.0 -y + conda install pytorch=2.6.0 -y conda install -c conda-forge libstdcxx-ng=14 -y conda install numpy=1.23.5 -y EXPECTED_VERSION_STRING="Python version is 3.11, NumPy version is 1.23.5, and PyTorch version is 2.6.0" From fa3361e77cb84c398ed85b777683c49c15d096b9 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Wed, 21 May 2025 14:07:51 -0700 Subject: [PATCH 038/230] =?UTF-8?q?TPRD-1536:=20Update=20'build.py'=20to?= =?UTF-8?q?=20meet=20TensorRT-LLM=20repository=20structur=E2=80=A6=20(#821?= =?UTF-8?q?4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/build.py b/build.py index e174c9b281..e95e27991f 100755 --- a/build.py +++ b/build.py @@ -622,7 +622,7 @@ def backend_cmake_args(images, components, be, install_dir, library_paths): cargs += cmake_backend_extra_args(be) if be == "tensorrtllm": - cargs.append("-S ../inflight_batcher_llm -B .") + cargs.append("-S ../triton_backend/inflight_batcher_llm -B .") else: cargs.append("..") @@ -2081,7 +2081,16 @@ def backend_build( cmake_script.comment() cmake_script.mkdir(build_dir) cmake_script.cwd(build_dir) - cmake_script.gitclone(backend_repo(be), tag, be, github_organization) + if be == "tensorrtllm": + github_organization = ( + "https://github.com/NVIDIA" + if "triton-inference-server" in FLAGS.github_organization + else FLAGS.github_organization + ) + repository_name = "TensorRT-LLM" + cmake_script.gitclone(repository_name, tag, be, github_organization) + else: + cmake_script.gitclone(backend_repo(be), tag, be, github_organization) if be == "tensorrtllm": tensorrtllm_prebuild(cmake_script) From 550f64b21f039fd88fb3041ebb419c7edde9679e Mon Sep 17 00:00:00 2001 From: Tushar Sharma Date: Wed, 21 May 2025 15:54:39 -0700 Subject: [PATCH 039/230] build: Convert vLLM index url ARGS into docker secrets (#8197) --- build.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/build.py b/build.py index e95e27991f..eaf00b6dd6 100755 --- a/build.py +++ b/build.py @@ -1481,12 +1481,12 @@ def dockerfile_prepare_container_linux(argmap, backends, enable_gpu, target_mach if "vllm" in backends: df += f""" +# Install required packages for vLLM models ARG BUILD_PUBLIC_VLLM="true" -ARG VLLM_INDEX_URL -ARG PYTORCH_TRITON_URL -ARG NVPL_SLIM_URL - RUN --mount=type=secret,id=req,target=/run/secrets/requirements \\ + --mount=type=secret,id=VLLM_INDEX_URL,env=VLLM_INDEX_URL \\ + --mount=type=secret,id=PYTORCH_TRITON_URL,env=PYTORCH_TRITON_URL \\ + --mount=type=secret,id=NVPL_SLIM_URL,env=NVPL_SLIM_URL \\ if [ "$BUILD_PUBLIC_VLLM" = "false" ]; then \\ if [ "$(uname -m)" = "x86_64" ]; then \\ pip3 install --no-cache-dir \\ @@ -1900,10 +1900,10 @@ def create_docker_build_script(script_name, container_install_dir, container_ci_ if secrets: finalargs += [ f"--secret id=req,src={requirements}", - f"--build-arg VLLM_INDEX_URL={vllm_index_url}", - f"--build-arg PYTORCH_TRITON_URL={pytorch_triton_url}", + f"--secret id=VLLM_INDEX_URL", + f"--secret id=PYTORCH_TRITON_URL", + f"--secret id=NVPL_SLIM_URL", f"--build-arg BUILD_PUBLIC_VLLM={build_public_vllm}", - f"--build-arg NVPL_SLIM_URL={nvpl_slim_url}", ] finalargs += [ "-t", @@ -2778,8 +2778,6 @@ def enable_all(): metavar=("key", "value"), help="Add build secrets in the form of . These secrets are used during the build process for vllm. The secrets are passed to the Docker build step as `--secret id=`. The following keys are expected and their purposes are described below:\n\n" " - 'req': A file containing a list of dependencies for pip (e.g., requirements.txt).\n" - " - 'vllm_index_url': The index URL for the pip install.\n" - " - 'pytorch_triton_url': The location of the PyTorch wheel to download.\n" " - 'build_public_vllm': A flag (default is 'true') indicating whether to build the public VLLM version.\n\n" "Ensure that the required environment variables for these secrets are set before running the build.", ) @@ -2901,9 +2899,6 @@ def enable_all(): secrets = dict(getattr(FLAGS, "build_secret", [])) if secrets: requirements = secrets.get("req", "") - vllm_index_url = secrets.get("vllm_index_url", "") - pytorch_triton_url = secrets.get("pytorch_triton_url", "") - nvpl_slim_url = secrets.get("nvpl_slim_url", "") build_public_vllm = secrets.get("build_public_vllm", "true") log('Build Arg for BUILD_PUBLIC_VLLM: "{}"'.format(build_public_vllm)) From 27cece93af0048a07b936dd3ffd5ea2e838f9098 Mon Sep 17 00:00:00 2001 From: Sai Kiran Polisetty Date: Thu, 29 May 2025 15:36:37 +0530 Subject: [PATCH 040/230] fix: Improve cancellation handling for gRPC non-decoupled inference (#8220) --- qa/L0_decoupled/decoupled_test.py | 240 +++++++++++++++++++++++++----- qa/L0_decoupled/test.sh | 2 +- src/grpc/infer_handler.cc | 37 ++--- 3 files changed, 221 insertions(+), 58 deletions(-) diff --git a/qa/L0_decoupled/decoupled_test.py b/qa/L0_decoupled/decoupled_test.py index d7bc59f5c7..edb5d8c48a 100755 --- a/qa/L0_decoupled/decoupled_test.py +++ b/qa/L0_decoupled/decoupled_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2020-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -32,6 +32,7 @@ import os import queue +import threading import time import unittest from functools import partial @@ -606,53 +607,212 @@ def test_wrong_shape(self): class NonDecoupledTest(tu.TestResultCollector): def setUp(self): self.model_name_ = "repeat_int32" - self.input_data = { - "IN": np.array([1], dtype=np.int32), - "DELAY": np.array([0], dtype=np.uint32), - "WAIT": np.array([0], dtype=np.uint32), + self.data_matrix = [ + # ("IN", "DELAY", "WAIT") + ([1], [0], [0]), + ([1], [4000], [2000]), + ([1], [2000], [4000]), + ] + + # For grpc async infer test + self.callback_error = None + self.callback_result = None + self.callback_invoked_event = threading.Event() + + def _input_data(self, in_value, delay_value, wait_value): + return { + "IN": np.array(in_value, dtype=np.int32), + "DELAY": np.array(delay_value, dtype=np.uint32), + "WAIT": np.array(wait_value, dtype=np.uint32), } + def _async_callback(self, result, error): + """Callback for async_infer.""" + self.callback_error = error + self.callback_result = result + self.callback_invoked_event.set() + def test_grpc(self): - inputs = [ - grpcclient.InferInput("IN", [1], "INT32").set_data_from_numpy( - self.input_data["IN"] - ), - grpcclient.InferInput("DELAY", [1], "UINT32").set_data_from_numpy( - self.input_data["DELAY"] - ), - grpcclient.InferInput("WAIT", [1], "UINT32").set_data_from_numpy( - self.input_data["WAIT"] - ), - ] + for in_value, delay_value, wait_value in self.data_matrix: + with self.subTest(IN=in_value, DELAY=delay_value, WAIT=wait_value): + input_data = self._input_data(in_value, delay_value, wait_value) + inputs = [ + grpcclient.InferInput("IN", [1], "INT32").set_data_from_numpy( + input_data["IN"] + ), + grpcclient.InferInput("DELAY", [1], "UINT32").set_data_from_numpy( + input_data["DELAY"] + ), + grpcclient.InferInput("WAIT", [1], "UINT32").set_data_from_numpy( + input_data["WAIT"] + ), + ] + + triton_client = grpcclient.InferenceServerClient( + url="localhost:8001", verbose=True + ) - triton_client = grpcclient.InferenceServerClient( - url="localhost:8001", verbose=True - ) - # Expect the inference is successful - res = triton_client.infer(model_name=self.model_name_, inputs=inputs) - self.assertEqual(1, res.as_numpy("OUT")[0]) - self.assertEqual(0, res.as_numpy("IDX")[0]) + # Expect the inference is successful + res = triton_client.infer(model_name=self.model_name_, inputs=inputs) + self.assertEqual(1, res.as_numpy("OUT")[0]) + self.assertEqual(0, res.as_numpy("IDX")[0]) def test_http(self): - inputs = [ - httpclient.InferInput("IN", [1], "INT32").set_data_from_numpy( - self.input_data["IN"] - ), - httpclient.InferInput("DELAY", [1], "UINT32").set_data_from_numpy( - self.input_data["DELAY"] - ), - httpclient.InferInput("WAIT", [1], "UINT32").set_data_from_numpy( - self.input_data["WAIT"] - ), + for in_value, delay_value, wait_value in self.data_matrix: + with self.subTest(IN=in_value, DELAY=delay_value, WAIT=wait_value): + input_data = self._input_data(in_value, delay_value, wait_value) + inputs = [ + httpclient.InferInput("IN", [1], "INT32").set_data_from_numpy( + input_data["IN"] + ), + httpclient.InferInput("DELAY", [1], "UINT32").set_data_from_numpy( + input_data["DELAY"] + ), + httpclient.InferInput("WAIT", [1], "UINT32").set_data_from_numpy( + input_data["WAIT"] + ), + ] + + triton_client = httpclient.InferenceServerClient( + url="localhost:8000", verbose=True + ) + + # Expect the inference is successful + res = triton_client.infer(model_name=self.model_name_, inputs=inputs) + self.assertEqual(1, res.as_numpy("OUT")[0]) + self.assertEqual(0, res.as_numpy("IDX")[0]) + + def test_grpc_async(self): + for in_value, delay_value, wait_value in self.data_matrix: + with self.subTest(IN=in_value, DELAY=delay_value, WAIT=wait_value): + input_data = self._input_data(in_value, delay_value, wait_value) + inputs = [ + grpcclient.InferInput("IN", [1], "INT32").set_data_from_numpy( + input_data["IN"] + ), + grpcclient.InferInput("DELAY", [1], "UINT32").set_data_from_numpy( + input_data["DELAY"] + ), + grpcclient.InferInput("WAIT", [1], "UINT32").set_data_from_numpy( + input_data["WAIT"] + ), + ] + + triton_client = grpcclient.InferenceServerClient( + url="localhost:8001", + verbose=True, + ) + + # Clear previous results + self.callback_error = None + self.callback_result = None + self.callback_invoked_event.clear() + + try: + triton_client.async_infer( + model_name=self.model_name_, + inputs=inputs, + callback=self._async_callback, + ) + except Exception as e: + self.fail(f"Failed to initiate async_infer: {e}") + continue + + # Wait for the callback to be invoked, with a timeout + self.assertTrue( + self.callback_invoked_event.wait(timeout=10), + "Callback not invoked within timeout.", + ) + + # Expect the inference is successful + self.assertIsNone( + self.callback_error, f"Inference failed: {self.callback_error}" + ) + self.assertIsNotNone(self.callback_result, "Inference result is None.") + self.assertEqual(1, self.callback_result.as_numpy("OUT")[0]) + self.assertEqual(0, self.callback_result.as_numpy("IDX")[0]) + + # Wait and check server/model health + time.sleep(5) + self.assertTrue(triton_client.is_model_ready(self.model_name_)) + + def test_grpc_async_cancel(self): + data_matrix = [ + # ("IN", "DELAY", "WAIT") + ([1], [4000], [2000]), + ([1], [2000], [4000]), ] - triton_client = httpclient.InferenceServerClient( - url="localhost:8000", verbose=True - ) - # Expect the inference is successful - res = triton_client.infer(model_name=self.model_name_, inputs=inputs) - self.assertEqual(1, res.as_numpy("OUT")[0]) - self.assertEqual(0, res.as_numpy("IDX")[0]) + for in_value, delay_value, wait_value in data_matrix: + with self.subTest(IN=in_value, DELAY=delay_value, WAIT=wait_value): + input_data = self._input_data(in_value, delay_value, wait_value) + inputs = [ + grpcclient.InferInput("IN", [1], "INT32").set_data_from_numpy( + input_data["IN"] + ), + grpcclient.InferInput("DELAY", [1], "UINT32").set_data_from_numpy( + input_data["DELAY"] + ), + grpcclient.InferInput("WAIT", [1], "UINT32").set_data_from_numpy( + input_data["WAIT"] + ), + ] + + triton_client = grpcclient.InferenceServerClient( + url="localhost:8001", + verbose=True, + ) + + # Clear previous results + self.callback_error = None + self.callback_result = None + self.callback_invoked_event.clear() + + request_handle = None + try: + request_handle = triton_client.async_infer( + model_name=self.model_name_, + inputs=inputs, + callback=self._async_callback, + ) + except Exception as e: + self.fail(f"Failed to initiate async_infer: {e}") + continue + + # Allow request to be fully initiated + time.sleep(0.5) + + # Attempt to cancel the request + if request_handle: + try: + request_handle.cancel() + except Exception as e: + self.fail(f"Error calling request_handle.cancel(): {e}") + continue + else: + self.fail("Invalid request_handle, cannot cancel.") + continue + + # Wait for the callback to be invoked + self.assertTrue( + self.callback_invoked_event.wait(timeout=10), + "Callback not invoked within timeout after cancellation.", + ) + + # Expect the inference is failed + self.assertIsInstance( + self.callback_error, + InferenceServerException, + f"Unexpected error type: {type(self.callback_error)}", + ) + self.assertIn( + "StatusCode.CANCELLED", + self.callback_error.status(), + ) + + # Wait and check server/model health + time.sleep(5) + self.assertTrue(triton_client.is_model_ready(self.model_name_)) if __name__ == "__main__": diff --git a/qa/L0_decoupled/test.sh b/qa/L0_decoupled/test.sh index 20ca0fffa3..839d0131aa 100755 --- a/qa/L0_decoupled/test.sh +++ b/qa/L0_decoupled/test.sh @@ -196,7 +196,7 @@ if [ $? -ne 0 ]; then echo -e "\n***\n*** Test NonDecoupledTest Failed\n***" RET=1 else - check_test_results $TEST_RESULT_FILE 2 + check_test_results $TEST_RESULT_FILE 4 if [ $? -ne 0 ]; then cat $CLIENT_LOG echo -e "\n***\n*** Test Result Verification Failed\n***" diff --git a/src/grpc/infer_handler.cc b/src/grpc/infer_handler.cc index 6d3b6ddcf3..2166f0c60e 100644 --- a/src/grpc/infer_handler.cc +++ b/src/grpc/infer_handler.cc @@ -1068,12 +1068,14 @@ ModelInferHandler::InferResponseComplete( state->cb_count_++; } + bool is_final_response = (flags & TRITONSERVER_RESPONSE_COMPLETE_FINAL) != 0; + LOG_VERBOSE(1) << "ModelInferHandler::InferResponseComplete, " << state->unique_id_ << " step " << state->step_; // Allow sending 1 response and final flag separately, only mark // non-inflight when seeing final flag - if (flags & TRITONSERVER_RESPONSE_COMPLETE_FINAL) { + if (is_final_response) { state->context_->EraseInflightState(state); } @@ -1093,22 +1095,23 @@ ModelInferHandler::InferResponseComplete( << ", skipping response generation as grpc transaction was " "cancelled... "; - if (state->delay_enqueue_ms_ != 0) { - // Will delay PutTaskBackToQueue by the specified time. - // This can be used to test the flow when cancellation request - // issued for the request during InferResponseComplete - // callback right before Process in the notification thread. - LOG_INFO << "Delaying PutTaskBackToQueue by " << state->delay_enqueue_ms_ - << " ms..."; - std::this_thread::sleep_for( - std::chrono::milliseconds(state->delay_enqueue_ms_)); - } - - // Send state back to the queue so that state can be released - // in the next cycle. - state->context_->PutTaskBackToQueue(state); + if (is_final_response) { + if (state->delay_enqueue_ms_ != 0) { + // Will delay PutTaskBackToQueue by the specified time. + // This can be used to test the flow when cancellation request + // issued for the request during InferResponseComplete + // callback right before Process in the notification thread. + LOG_INFO << "Delaying PutTaskBackToQueue by " + << state->delay_enqueue_ms_ << " ms..."; + std::this_thread::sleep_for( + std::chrono::milliseconds(state->delay_enqueue_ms_)); + } - delete response_release_payload; + // Send state back to the queue so that state can be released + // in the next cycle. + state->context_->PutTaskBackToQueue(state); + delete response_release_payload; + } return; } @@ -1156,7 +1159,7 @@ ModelInferHandler::InferResponseComplete( // Defer sending the response until FINAL flag is seen or // there is error - if ((flags & TRITONSERVER_RESPONSE_COMPLETE_FINAL) == 0) { + if (!is_final_response) { return; } From b343cc0a6c895385331717cb00f5b075c112da06 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Tue, 3 Jun 2025 13:05:53 -0700 Subject: [PATCH 041/230] test: fix L0_trace flaky test (#8231) --- qa/L0_trace/test.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qa/L0_trace/test.sh b/qa/L0_trace/test.sh index 8322e28d68..7b29ea017a 100755 --- a/qa/L0_trace/test.sh +++ b/qa/L0_trace/test.sh @@ -1087,6 +1087,9 @@ for p in {1..10}; do sleep 10 done +# Wait for all traces to be collected +sleep 5 + if ! [[ -s collected_traces.json && `grep -c "\"parentSpanId\":\"\"" ./collected_traces.json` == 1 && `grep -c "\"parentSpanId\":\"b7ad6b7169242424\"" ./collected_traces.json` == 10 ]] ; then echo -e "\n***\n*** collected_traces.json should contain 11 OTel trace, but it is not. \n***" exit 1 From 1b8c81848fb5d45ab25a97fed87b9f762bac09da Mon Sep 17 00:00:00 2001 From: Dmitry Tokarev Date: Thu, 5 Jun 2025 19:39:21 -0400 Subject: [PATCH 042/230] Prepare r25.05 for merging to main (#8237) Co-authored-by: Kris Hung Co-authored-by: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Co-authored-by: richardhuo-nv --- Dockerfile.sdk | 2 +- README.md | 11 +++++------ TRITON_VERSION | 2 +- build.py | 2 +- deploy/aws/values.yaml | 2 +- deploy/fleetcommand/Chart.yaml | 2 +- deploy/fleetcommand/values.yaml | 6 +++--- deploy/gcp/values.yaml | 2 +- .../benchmark/perf-analyzer-script/triton_client.yaml | 2 +- .../server-deployer/build_and_push.sh | 6 +++--- .../server-deployer/chart/triton/Chart.yaml | 4 ++-- .../server-deployer/chart/triton/values.yaml | 6 +++--- .../server-deployer/data-test/schema.yaml | 2 +- .../gke-marketplace-app/server-deployer/schema.yaml | 4 ++-- deploy/gke-marketplace-app/trt-engine/README.md | 6 +++--- deploy/k8s-onprem/values.yaml | 2 +- deploy/oci/values.yaml | 2 +- .../model_repository/inception_onnx/config.pbtxt | 8 +++++--- docs/introduction/compatibility.md | 3 +++ docs/introduction/release_notes.md | 6 +++--- python/openai/README.md | 2 +- qa/L0_infer/test.sh | 2 +- qa/common/gen_jetson_trt_models | 2 +- qa/common/gen_qa_custom_ops | 2 +- qa/common/gen_qa_model_repository | 2 +- 25 files changed, 47 insertions(+), 43 deletions(-) diff --git a/Dockerfile.sdk b/Dockerfile.sdk index 65d46a0f4d..33414c3fe4 100644 --- a/Dockerfile.sdk +++ b/Dockerfile.sdk @@ -29,7 +29,7 @@ # # Base image on the minimum Triton container -ARG BASE_IMAGE=nvcr.io/nvidia/tritonserver:25.04-py3-min +ARG BASE_IMAGE=nvcr.io/nvidia/tritonserver:25.05-py3-min ARG TRITON_CLIENT_REPO_SUBDIR=clientrepo ARG TRITON_PA_REPO_SUBDIR=perfanalyzerrepo diff --git a/README.md b/README.md index 78b50aff5f..2de74ae96a 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ >[!WARNING] >You are currently on the `main` branch which tracks under-development progress ->towards the next release. The current release is version [2.57.0](https://github.com/triton-inference-server/server/releases/latest) ->and corresponds to the 25.04 container release on NVIDIA GPU Cloud (NGC). +>towards the next release. The current release is version [2.58.0](https://github.com/triton-inference-server/server/releases/latest) +>and corresponds to the 25.05 container release on NVIDIA GPU Cloud (NGC). # Triton Inference Server @@ -90,16 +90,16 @@ Inference Server with the ```bash # Step 1: Create the example model repository -git clone -b r25.02 https://github.com/triton-inference-server/server.git +git clone -b r25.05 https://github.com/triton-inference-server/server.git cd server/docs/examples ./fetch_models.sh # Step 2: Launch triton from the NGC Triton container -docker run --gpus=1 --rm --net=host -v ${PWD}/model_repository:/models nvcr.io/nvidia/tritonserver:25.02-py3 tritonserver --model-repository=/models --model-control-mode explicit --load-model densenet_onnx +docker run --gpus=1 --rm --net=host -v ${PWD}/model_repository:/models nvcr.io/nvidia/tritonserver:25.05-py3 tritonserver --model-repository=/models --model-control-mode explicit --load-model densenet_onnx # Step 3: Sending an Inference Request # In a separate console, launch the image_client example from the NGC Triton SDK container -docker run -it --rm --net=host nvcr.io/nvidia/tritonserver:25.02-py3-sdk /workspace/install/bin/image_client -m densenet_onnx -c 3 -s INCEPTION /workspace/images/mug.jpg +docker run -it --rm --net=host nvcr.io/nvidia/tritonserver:25.05-py3-sdk /workspace/install/bin/image_client -m densenet_onnx -c 3 -s INCEPTION /workspace/images/mug.jpg # Inference should return the following Image '/workspace/images/mug.jpg': @@ -260,4 +260,3 @@ For questions, we recommend posting in our community Please refer to the [NVIDIA Developer Triton page](https://developer.nvidia.com/nvidia-triton-inference-server) for more information. - diff --git a/TRITON_VERSION b/TRITON_VERSION index 7bf8e9ee0c..6d43c77ac3 100644 --- a/TRITON_VERSION +++ b/TRITON_VERSION @@ -1 +1 @@ -2.58.0dev +2.59.0dev diff --git a/build.py b/build.py index eaf00b6dd6..150719ccc0 100755 --- a/build.py +++ b/build.py @@ -73,7 +73,7 @@ DEFAULT_TRITON_VERSION_MAP = { "release_version": "2.58.0dev", "triton_container_version": "25.05dev", - "upstream_container_version": "25.04", + "upstream_container_version": "25.05", "ort_version": "1.22.0", "ort_openvino_version": "2025.1.0", "standalone_openvino_version": "2025.1.0", diff --git a/deploy/aws/values.yaml b/deploy/aws/values.yaml index 1e0f283b1d..cddaaaf3a6 100644 --- a/deploy/aws/values.yaml +++ b/deploy/aws/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.04-py3 + imageName: nvcr.io/nvidia/tritonserver:25.05-py3 pullPolicy: IfNotPresent modelRepositoryPath: s3://triton-inference-server-repository/model_repository numGpus: 1 diff --git a/deploy/fleetcommand/Chart.yaml b/deploy/fleetcommand/Chart.yaml index fe6125a34e..0664a5d959 100644 --- a/deploy/fleetcommand/Chart.yaml +++ b/deploy/fleetcommand/Chart.yaml @@ -26,7 +26,7 @@ apiVersion: v1 # appVersion is the Triton version; update when changing release -appVersion: "2.57.0" +appVersion: "2.58.0" description: Triton Inference Server (Fleet Command) name: triton-inference-server # version is the Chart version; update when changing anything in the chart diff --git a/deploy/fleetcommand/values.yaml b/deploy/fleetcommand/values.yaml index e149a48561..b4044b795a 100644 --- a/deploy/fleetcommand/values.yaml +++ b/deploy/fleetcommand/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.04-py3 + imageName: nvcr.io/nvidia/tritonserver:25.05-py3 pullPolicy: IfNotPresent numGpus: 1 serverCommand: tritonserver @@ -47,13 +47,13 @@ image: # # To set model control mode, uncomment and configure below # TODO: Fix the following url, it is invalid - # See https://github.com/triton-inference-server/server/blob/r25.04/docs/user_guide/model_management.md + # See https://github.com/triton-inference-server/server/blob/r25.05/docs/user_guide/model_management.md # for more details #- --model-control-mode=explicit|poll|none # # Additional server args # - # see https://github.com/triton-inference-server/server/blob/r25.04/README.md + # see https://github.com/triton-inference-server/server/blob/r25.05/README.md # for more details service: diff --git a/deploy/gcp/values.yaml b/deploy/gcp/values.yaml index 3b6d07e771..3770934cc8 100644 --- a/deploy/gcp/values.yaml +++ b/deploy/gcp/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.04-py3 + imageName: nvcr.io/nvidia/tritonserver:25.05-py3 pullPolicy: IfNotPresent modelRepositoryPath: gs://triton-inference-server-repository/model_repository numGpus: 1 diff --git a/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml b/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml index b3c136898c..38a65ed8e8 100644 --- a/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml +++ b/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml @@ -33,7 +33,7 @@ metadata: namespace: default spec: containers: - - image: nvcr.io/nvidia/tritonserver:25.04-py3-sdk + - image: nvcr.io/nvidia/tritonserver:25.05-py3-sdk imagePullPolicy: Always name: nv-triton-client securityContext: diff --git a/deploy/gke-marketplace-app/server-deployer/build_and_push.sh b/deploy/gke-marketplace-app/server-deployer/build_and_push.sh index 237b70b179..6cb6f76e0a 100755 --- a/deploy/gke-marketplace-app/server-deployer/build_and_push.sh +++ b/deploy/gke-marketplace-app/server-deployer/build_and_push.sh @@ -27,9 +27,9 @@ export REGISTRY=gcr.io/$(gcloud config get-value project | tr ':' '/') export APP_NAME=tritonserver -export MAJOR_VERSION=2.57 -export MINOR_VERSION=2.57.0 -export NGC_VERSION=25.04-py3 +export MAJOR_VERSION=2.58 +export MINOR_VERSION=2.58.0 +export NGC_VERSION=25.05-py3 docker pull nvcr.io/nvidia/$APP_NAME:$NGC_VERSION diff --git a/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml b/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml index 82cd327fd4..5011733bbc 100644 --- a/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml +++ b/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml @@ -25,7 +25,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. apiVersion: v1 -appVersion: "2.57" +appVersion: "2.58" description: Triton Inference Server name: triton-inference-server -version: 2.57.0 +version: 2.58.0 diff --git a/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml b/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml index f6a61d6f6c..ed69180192 100644 --- a/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml +++ b/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml @@ -31,14 +31,14 @@ maxReplicaCount: 3 tritonProtocol: HTTP # HPA GPU utilization autoscaling target HPATargetAverageValue: 85 -modelRepositoryPath: gs://triton_sample_models/25.04 -publishedVersion: '2.57.0' +modelRepositoryPath: gs://triton_sample_models/25.05 +publishedVersion: '2.58.0' gcpMarketplace: true image: registry: gcr.io repository: nvidia-ngc-public/tritonserver - tag: 25.04-py3 + tag: 25.05-py3 pullPolicy: IfNotPresent # modify the model repository here to match your GCP storage bucket numGpus: 1 diff --git a/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml b/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml index 79ee70d01e..2c55441213 100644 --- a/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml +++ b/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml @@ -27,7 +27,7 @@ x-google-marketplace: schemaVersion: v2 applicationApiVersion: v1beta1 - publishedVersion: '2.57.0' + publishedVersion: '2.58.0' publishedVersionMetadata: releaseNote: >- Initial release. diff --git a/deploy/gke-marketplace-app/server-deployer/schema.yaml b/deploy/gke-marketplace-app/server-deployer/schema.yaml index 1cff4c3c40..68c171d28a 100644 --- a/deploy/gke-marketplace-app/server-deployer/schema.yaml +++ b/deploy/gke-marketplace-app/server-deployer/schema.yaml @@ -27,7 +27,7 @@ x-google-marketplace: schemaVersion: v2 applicationApiVersion: v1beta1 - publishedVersion: '2.57.0' + publishedVersion: '2.58.0' publishedVersionMetadata: releaseNote: >- Initial release. @@ -89,7 +89,7 @@ properties: modelRepositoryPath: type: string title: Bucket where models are stored. Please make sure the user/service account to create the GKE app has permission to this GCS bucket. Read Triton documentation on configs and formatting details, supporting TensorRT, TensorFlow, Pytorch, Onnx ... etc. - default: gs://triton_sample_models/25.04 + default: gs://triton_sample_models/25.05 image.ldPreloadPath: type: string title: Leave this empty by default. Triton allows users to create custom layers for backend such as TensorRT plugin or Tensorflow custom ops, the compiled shared library must be provided via LD_PRELOAD environment variable. diff --git a/deploy/gke-marketplace-app/trt-engine/README.md b/deploy/gke-marketplace-app/trt-engine/README.md index ff7e5cdaef..01475e551c 100644 --- a/deploy/gke-marketplace-app/trt-engine/README.md +++ b/deploy/gke-marketplace-app/trt-engine/README.md @@ -33,7 +33,7 @@ ``` docker run --gpus all -it --network host \ --shm-size=1g --ulimit memlock=-1 --ulimit stack=67108864 \ - -v ~:/scripts nvcr.io/nvidia/tensorrt:25.04-py3 + -v ~:/scripts nvcr.io/nvidia/tensorrt:25.05-py3 pip install onnx six torch tf2onnx tensorflow @@ -57,7 +57,7 @@ mkdir -p engines python3 builder.py -m models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/model.ckpt -o engines/bert_large_int8_bs1_s128.engine -b 1 -s 128 -c models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/ -v models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/vocab.txt --int8 --fp16 --strict --calib-num 1 -iln -imh -gsutil cp bert_large_int8_bs1_s128.engine gs://triton_sample_models/25.04/bert/1/model.plan +gsutil cp bert_large_int8_bs1_s128.engine gs://triton_sample_models/25.05/bert/1/model.plan ``` -For each Triton upgrade, container version used to generate the model, and the model path in GCS `gs://triton_sample_models/25.04/` should be updated accordingly with the correct version. +For each Triton upgrade, container version used to generate the model, and the model path in GCS `gs://triton_sample_models/25.05/` should be updated accordingly with the correct version. diff --git a/deploy/k8s-onprem/values.yaml b/deploy/k8s-onprem/values.yaml index 442288b5a7..27cb9bf33b 100644 --- a/deploy/k8s-onprem/values.yaml +++ b/deploy/k8s-onprem/values.yaml @@ -30,7 +30,7 @@ tags: openshift: false image: - imageName: nvcr.io/nvidia/tritonserver:25.04-py3 + imageName: nvcr.io/nvidia/tritonserver:25.05-py3 pullPolicy: IfNotPresent modelRepositoryServer: < Replace with the IP Address of your file server > modelRepositoryPath: /srv/models diff --git a/deploy/oci/values.yaml b/deploy/oci/values.yaml index 88be9847d3..b082864f3e 100644 --- a/deploy/oci/values.yaml +++ b/deploy/oci/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.04-py3 + imageName: nvcr.io/nvidia/tritonserver:25.05-py3 pullPolicy: IfNotPresent modelRepositoryPath: s3://https://.compat.objectstorage..oraclecloud.com:443/triton-inference-server-repository numGpus: 1 diff --git a/docs/examples/model_repository/inception_onnx/config.pbtxt b/docs/examples/model_repository/inception_onnx/config.pbtxt index c4f11b41ef..46056cb1ea 100644 --- a/docs/examples/model_repository/inception_onnx/config.pbtxt +++ b/docs/examples/model_repository/inception_onnx/config.pbtxt @@ -25,20 +25,22 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. name: "inception_onnx" platform: "onnxruntime_onnx" -max_batch_size: 128 +max_batch_size: 0 input [ { - name: "input" + name: "input:0" data_type: TYPE_FP32 format: FORMAT_NHWC dims: [ 299, 299, 3 ] + reshape { shape: [ 1, 299, 299, 3 ] } } ] output [ { - name: "InceptionV3/Predictions/Softmax" + name: "InceptionV3/Predictions/Softmax:0" data_type: TYPE_FP32 dims: [ 1001 ] + reshape { shape: [ 1, 1001 ] } label_filename: "inception_labels.txt" } ] diff --git a/docs/introduction/compatibility.md b/docs/introduction/compatibility.md index 1fafebd88b..afa4ab0ac8 100644 --- a/docs/introduction/compatibility.md +++ b/docs/introduction/compatibility.md @@ -38,6 +38,7 @@ | Triton release version | NGC Tag | Python version | Torch version | TensorRT version | TensorRT-LLM version | CUDA version | CUDA Driver version | Size | | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 25.05 | nvcr.io/nvidia/tritonserver:25.05-trtllm-python-py3 | Python 3.12.3 | 2.7.0a0+7c8ec84dab.nv25.3 | 10.9.0.34 | 0.19.0 | 12.8.1.012 | 570.124.06 | 17G | | 25.04 | nvcr.io/nvidia/tritonserver:25.04-trtllm-python-py3 | Python 3.12.3 | 2.7.0a0+7c8ec84dab.nv25.3 | 10.9.0.34 | 0.18.2 | 12.8.1.012 | 570.124.06 | 17G | | 25.03 | nvcr.io/nvidia/tritonserver:25.03-trtllm-python-py3 | Python 3.12.3 | 2.7.0a0%2B7c8ec84dab.nv25.3 | 10.9.0.34 | 0.18.0 | 12.8.1.012 | 570.124.06 | 28G | | 25.02 | nvcr.io/nvidia/tritonserver:25.02-trtllm-python-py3 | Python 3.12.3 | 2.6.0a0%2Becf3bae40a.nv25.1 | 10.8.0.43 | 0.17.0.post1 | 12.8.0.038 | 570.86.10 | 28G | @@ -56,6 +57,7 @@ | Triton release version | NGC Tag | Python version | vLLM version | CUDA version | CUDA Driver version | Size | | --- | --- | --- | --- | --- | --- | --- | +| 25.05 | nvcr.io/nvidia/tritonserver:25.04-vllm-python-py3 | Python 3.12.3 | 0.8.4+c4369543.nv25.5.cu129 | 12.9.0.043 | 575.51.03 | 10G | | 25.04 | nvcr.io/nvidia/tritonserver:25.04-vllm-python-py3 | Python 3.12.3 | 0.8.1+5f4af9e0.nv25.4.cu129 | 12.9.0.036 | 575.51.02 | 10G | | 25.03 | nvcr.io/nvidia/tritonserver:25.03-vllm-python-py3 | Python 3.12.3 | 0.7.3+04de634a.nv25.3.cu128 | 12.8.1.012 | 570.124.06 | 22G | | 25.02 | nvcr.io/nvidia/tritonserver:25.02-vllm-python-py3 | Python 3.12.3 | 0.7.0+5e800e3d.nv25.2.cu128 | 12.8.0.038 | 570.86.10 | 22G | @@ -74,6 +76,7 @@ | Triton release version | ONNX Runtime | | --- | --- | +| 25.05 | 1.22.0 | | 25.04 | 1.21.0 | | 25.03 | 1.21.0 | | 25.02 | 1.20.1 | diff --git a/docs/introduction/release_notes.md b/docs/introduction/release_notes.md index 105cf44ca7..f297ec3ac2 100644 --- a/docs/introduction/release_notes.md +++ b/docs/introduction/release_notes.md @@ -25,10 +25,10 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --> -# [Triton Inference Server Release 25.04](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-25-04.html#rel-25-04) +# [Triton Inference Server Release 25.05](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-25-05.html#rel-25-05) -The Triton Inference Server container image, release 25.04, is available +The Triton Inference Server container image, release 25.05, is available on [NGC](https://ngc.nvidia.com/catalog/containers/nvidia:tritonserver) and is open source on [GitHub](https://github.com/triton-inference-server/server). Release notes can -be found on the [GitHub Release Page](https://github.com/triton-inference-server/server/releases) \ No newline at end of file +be found on the [GitHub Release Page](https://github.com/triton-inference-server/server/releases) diff --git a/python/openai/README.md b/python/openai/README.md index 08d1780c84..51d8c5db72 100644 --- a/python/openai/README.md +++ b/python/openai/README.md @@ -51,7 +51,7 @@ docker run -it --net=host --gpus all --rm \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ -e HF_TOKEN \ - nvcr.io/nvidia/tritonserver:25.04-vllm-python-py3 + nvcr.io/nvidia/tritonserver:25.05-vllm-python-py3 ``` 2. Launch the OpenAI-compatible Triton Inference Server: diff --git a/qa/L0_infer/test.sh b/qa/L0_infer/test.sh index 79ba093ffd..939d2b948c 100755 --- a/qa/L0_infer/test.sh +++ b/qa/L0_infer/test.sh @@ -73,7 +73,7 @@ fi if [ "$TEST_SYSTEM_SHARED_MEMORY" -eq 1 ] || [ "$TEST_CUDA_SHARED_MEMORY" -eq 1 ]; then EXPECTED_NUM_TESTS=${EXPECTED_NUM_TESTS:="33"} else - EXPECTED_NUM_TESTS=${EXPECTED_NUM_TESTS:="44"} + EXPECTED_NUM_TESTS=${EXPECTED_NUM_TESTS:="46"} fi TEST_JETSON=${TEST_JETSON:=0} diff --git a/qa/common/gen_jetson_trt_models b/qa/common/gen_jetson_trt_models index f9d56c2815..8726e7c271 100755 --- a/qa/common/gen_jetson_trt_models +++ b/qa/common/gen_jetson_trt_models @@ -34,7 +34,7 @@ # Make all generated files accessible outside of container umask 0000 # Set the version of the models -TRITON_VERSION=${TRITON_VERSION:=25.04} +TRITON_VERSION=${TRITON_VERSION:=25.05} # Set the CUDA device to use CUDA_DEVICE=${RUNNER_ID:=0} # Set TensorRT image diff --git a/qa/common/gen_qa_custom_ops b/qa/common/gen_qa_custom_ops index e12cd89960..96801e4470 100755 --- a/qa/common/gen_qa_custom_ops +++ b/qa/common/gen_qa_custom_ops @@ -37,7 +37,7 @@ ## ############################################################################ -TRITON_VERSION=${TRITON_VERSION:=25.04} +TRITON_VERSION=${TRITON_VERSION:=25.05} NVIDIA_UPSTREAM_VERSION=${NVIDIA_UPSTREAM_VERSION:=$TRITON_VERSION} PYTORCH_IMAGE=${PYTORCH_IMAGE:=nvcr.io/nvidia/pytorch:$NVIDIA_UPSTREAM_VERSION-py3} UBUNTU_IMAGE=${UBUNTU_IMAGE:=ubuntu:24.04} diff --git a/qa/common/gen_qa_model_repository b/qa/common/gen_qa_model_repository index 6d3db26edc..7f4741d1c7 100755 --- a/qa/common/gen_qa_model_repository +++ b/qa/common/gen_qa_model_repository @@ -48,7 +48,7 @@ ## ############################################################################ -TRITON_VERSION=${TRITON_VERSION:=25.04} +TRITON_VERSION=${TRITON_VERSION:=25.05} # ONNX. Use ONNX_OPSET 0 to use the default for ONNX version ONNX_VERSION=1.16.1 From 17fcee4ab06d929acb764aac9b9c83c57daf2150 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Fri, 6 Jun 2025 15:34:05 -0700 Subject: [PATCH 043/230] test: Write logs to correct output stream (#8236) --- qa/L0_logging/test.sh | 51 +++++++++++++++++++++++++++++++++++++++++-- qa/common/util.sh | 9 ++++++-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/qa/L0_logging/test.sh b/qa/L0_logging/test.sh index a25693cf0e..4dabe976e6 100755 --- a/qa/L0_logging/test.sh +++ b/qa/L0_logging/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -490,7 +490,7 @@ set -e kill $SERVER_PID wait $SERVER_PID -#Test Negative Test Cases +# Test Negative Test Cases SERVER_ARGS="--log-warn="false" --model-repository=$MODELSDIR" SERVER_LOG="./server.log" run_server @@ -607,6 +607,53 @@ fi set -e +# Test Log Output Stream +# Set up an invalid model with a leading zero in the version number. This will print warning and error logs. +MODELSDIR_INVALID=`pwd`/log_models_invalid +rm -rf $MODELSDIR_INVALID && \ + cp -r $MODELSDIR $MODELSDIR_INVALID && \ + mv $MODELSDIR_INVALID/simple/1 $MODELSDIR_INVALID/simple/01 + +rm -f log_file.log +LOG_REGEX="(?P\d{2})(?P\d{2}) (?P\d{2}:\d{2}:\d{2}\.\d{6}) (?P\d+) (?P[\w\.]+):(?P\d+)] (?P.*)" +SERVER_ARGS="--log-verbose=1 --model-repository=$MODELSDIR_INVALID" +SERVER_LOG="./inference_server_log_file.log" +SERVER_ERROR_LOG="./inference_server_error_log_file.log" +run_server +if [ "$SERVER_PID" != "0" ]; then + echo -e "*** FAILED: unexpected success starting $SERVER" >> $CLIENT_LOG + cat $SERVER_LOG + kill_server + exit 1 +fi + +set +e +# Only INFO logs in SERVER_LOG +if [ `grep -c -P "I$LOG_REGEX" $SERVER_LOG` == "0" ]; then + echo -e "\n***\n*** Test Failed: INFO logs are not written to $SERVER_LOG\n***" + RET=1 +fi +if [ `grep -c -P "(W|E)$LOG_REGEX" $SERVER_LOG` != "0" ]; then + echo -e "\n***\n*** Test Failed: WARNING/ERROR logs are written to $SERVER_LOG\n***" + RET=1 +fi +# Only WARNING and ERROR logs in SERVER_ERROR_LOG +if [ `grep -c -P "I$LOG_REGEX" $SERVER_ERROR_LOG` != "0" ]; then + echo -e "\n***\n*** Test Failed: INFO logs are written to $SERVER_ERROR_LOG\n***" + RET=1 +fi +if [ `grep -c -P "W$LOG_REGEX" $SERVER_ERROR_LOG` == "0" ]; then + echo -e "\n***\n*** Test Failed: ERROR logs are not written to $SERVER_ERROR_LOG\n***" + RET=1 +fi +if [ `grep -c -P "E$LOG_REGEX" $SERVER_ERROR_LOG` == "0" ]; then + echo -e "\n***\n*** Test Failed: ERROR logs are not written to $SERVER_ERROR_LOG\n***" + RET=1 +fi + +unset $SERVER_ERROR_LOG +set -e + if [ $RET -eq 0 ]; then echo -e "\n***\n*** Test Passed\n***" else diff --git a/qa/common/util.sh b/qa/common/util.sh index 3874916573..08d10d7ed0 100755 --- a/qa/common/util.sh +++ b/qa/common/util.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2018-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -196,7 +196,12 @@ function run_server () { echo "=== Running LD_PRELOAD=$SERVER_LD_PRELOAD $SERVER $SERVER_ARGS" fi - LD_PRELOAD=$SERVER_LD_PRELOAD:${LD_PRELOAD} $SERVER $SERVER_ARGS > $SERVER_LOG 2>&1 & + # If SERVER_ERROR_LOG is not set, redirect stderr to stdout + if [ -z "${SERVER_ERROR_LOG:-}" ]; then + LD_PRELOAD=$SERVER_LD_PRELOAD:${LD_PRELOAD} $SERVER $SERVER_ARGS > $SERVER_LOG 2>&1 & + else + LD_PRELOAD=$SERVER_LD_PRELOAD:${LD_PRELOAD} $SERVER $SERVER_ARGS > $SERVER_LOG 2>$SERVER_ERROR_LOG & + fi SERVER_PID=$! wait_for_server_ready $SERVER_PID $SERVER_TIMEOUT From af8bb93026d67c8a19e1072def193feececc2917 Mon Sep 17 00:00:00 2001 From: mattwittwer Date: Fri, 6 Jun 2025 15:52:07 -0700 Subject: [PATCH 044/230] fix: Update handling of large array sizes (#8174) --- qa/L0_http/http_input_size_limit_test.py | 365 +++++++++++++++++++++++ qa/L0_http/test.sh | 102 +++++++ src/command_line_parser.cc | 14 + src/command_line_parser.h | 5 +- src/common.h | 3 + src/http_server.cc | 37 ++- src/http_server.h | 4 +- src/main.cc | 4 +- 8 files changed, 525 insertions(+), 9 deletions(-) create mode 100755 qa/L0_http/http_input_size_limit_test.py diff --git a/qa/L0_http/http_input_size_limit_test.py b/qa/L0_http/http_input_size_limit_test.py new file mode 100755 index 0000000000..35c9d34c0f --- /dev/null +++ b/qa/L0_http/http_input_size_limit_test.py @@ -0,0 +1,365 @@ +#!/usr/bin/python +# Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import sys + +sys.path.append("../common") + +import unittest + +import numpy as np +import requests +import test_util as tu + +# Constants for size calculations +# Each FP32 value is 4 bytes, so we need to divide target byte sizes by 4 to get element counts +BYTES_PER_FP32 = 4 +MB = 2**20 # 1 MB = 1,048,576 bytes +DEFAULT_LIMIT_BYTES = 64 * MB # 64MB default limit +INCREASED_LIMIT_BYTES = 128 * MB # 128MB increased limit + +# Calculate element counts for size limits +DEFAULT_LIMIT_ELEMENTS = DEFAULT_LIMIT_BYTES // BYTES_PER_FP32 # 16,777,216 elements +INCREASED_LIMIT_ELEMENTS = ( + INCREASED_LIMIT_BYTES // BYTES_PER_FP32 +) # 33,554,432 elements + +# Small offsets to go just over/under the limits +OFFSET_ELEMENTS = 32 + + +class InferSizeLimitTest(tu.TestResultCollector): + def _get_infer_url(self, model_name): + return "http://localhost:8000/v2/models/{}/infer".format(model_name) + + def test_default_limit_rejection_raw_binary(self): + """Test raw binary inputs with default limit""" + model = "onnx_zero_1_float32" + + # Test case 1: Input just over the 64MB limit (should fail) + # (2^24 + 32) elements * 4 bytes = 64MB + 128 bytes = 67,108,992 bytes + large_input = np.ones( + DEFAULT_LIMIT_ELEMENTS + OFFSET_ELEMENTS, dtype=np.float32 + ) + input_bytes = large_input.tobytes() + assert len(input_bytes) > 64 * MB # Verify we're actually over the 64MB limit + + headers = {"Inference-Header-Content-Length": "0"} + response = requests.post( + self._get_infer_url(model), data=input_bytes, headers=headers + ) + + # Should fail with 400 bad request with default limit + self.assertEqual( + 400, + response.status_code, + "Expected error code for oversized request, got: {}".format( + response.status_code + ), + ) + + # Verify error message contains size limit info + error_msg = response.content.decode() + self.assertIn( + "exceeds the maximum allowed value", + error_msg, + "Expected error message about exceeding max input size", + ) + + # Test case 2: Input just under the 64MB limit (should succeed) + # (2^24 - 32) elements * 4 bytes = 64MB - 128 bytes = 67,108,736 bytes + small_input = np.ones( + DEFAULT_LIMIT_ELEMENTS - OFFSET_ELEMENTS, dtype=np.float32 + ) + input_bytes = small_input.tobytes() + assert len(input_bytes) < 64 * MB # Verify we're actually under the 64MB limit + + response = requests.post( + self._get_infer_url(model), data=input_bytes, headers=headers + ) + + # Should succeed with 200 OK + self.assertEqual( + 200, + response.status_code, + "Expected success code for request within size limit, got: {}".format( + response.status_code + ), + ) + + # Verify output matches our input (identity model) + header_size = int(response.headers["Inference-Header-Content-Length"]) + output_data = response.content[header_size:] + + # Convert output bytes back to numpy array for comparison + output_array = np.frombuffer(output_data, dtype=np.float32) + self.assertTrue( + np.array_equal(output_array, small_input), + "Response data does not match input data", + ) + + def test_default_limit_rejection_json(self): + """Test JSON inputs with default limit""" + model = "onnx_zero_1_float32" + + # Test case 1: Input just over the 64MB limit (should fail) + # (2^24 + 32) elements * 4 bytes = 64MB + 128 bytes = 67,108,992 bytes + shape_size = DEFAULT_LIMIT_ELEMENTS + OFFSET_ELEMENTS + + payload = { + "inputs": [ + { + "name": "INPUT0", + "datatype": "FP32", + "shape": [1, shape_size], + "data": [1.0] * shape_size, + } + ] + } + assert ( + shape_size * BYTES_PER_FP32 > 64 * MB + ) # Verify we're actually over the 64MB limit + + headers = {"Content-Type": "application/json"} + response = requests.post( + self._get_infer_url(model), headers=headers, json=payload + ) + + # Should fail with 400 bad request with default limit + self.assertEqual( + 400, + response.status_code, + "Expected error code for oversized JSON request, got: {}".format( + response.status_code + ), + ) + + # Verify error message contains size limit info + error_msg = response.content.decode() + self.assertIn( + "exceeds the maximum allowed value", + error_msg, + "Expected error message about exceeding max input size", + ) + + # Test case 2: Input just under the 64MB limit (should succeed) + # (2^24 - 32) elements * 4 bytes = 64MB - 128 bytes = 67,108,736 bytes + shape_size = DEFAULT_LIMIT_ELEMENTS - OFFSET_ELEMENTS + + payload = { + "inputs": [ + { + "name": "INPUT0", + "datatype": "FP32", + "shape": [1, shape_size], + "data": [1.0] * shape_size, + } + ] + } + assert ( + shape_size * BYTES_PER_FP32 < 64 * MB + ) # Verify we're actually under the 64MB limit + + response = requests.post( + self._get_infer_url(model), headers=headers, json=payload + ) + + # Should succeed with 200 OK + self.assertEqual( + 200, + response.status_code, + "Expected success code for JSON request within size limit, got: {}".format( + response.status_code + ), + ) + + # Verify we got a valid response + result = response.json() + self.assertIn("outputs", result, "Response missing outputs field") + self.assertEqual(1, len(result["outputs"]), "Expected 1 output") + self.assertEqual( + shape_size, + result["outputs"][0]["shape"][1], + f"Expected shape {[1, shape_size]}, got {result['outputs'][0]['shape']}", + ) + + def test_large_input_raw_binary(self): + """Test raw binary input larger with custom limit set""" + model = "onnx_zero_1_float32" + + # Test case 1: Input just over the 128MB configured limit (should fail) + # (2^25 + 32) elements * 4 bytes = 128MB + 128 bytes = 134,217,856 bytes + large_input = np.ones( + INCREASED_LIMIT_ELEMENTS + OFFSET_ELEMENTS, dtype=np.float32 + ) + input_bytes = large_input.tobytes() + assert len(input_bytes) > 128 * MB # Verify we're actually over the 128MB limit + + headers = {"Inference-Header-Content-Length": "0"} + response = requests.post( + self._get_infer_url(model), data=input_bytes, headers=headers + ) + + # Should fail with 400 bad request with our increased limit + self.assertEqual( + 400, + response.status_code, + "Expected error code for oversized request, got: {}".format( + response.status_code + ), + ) + + # Verify error message contains size limit info + error_msg = response.content.decode() + self.assertIn( + "exceeds the maximum allowed value", + error_msg, + "Expected error message about exceeding max input size", + ) + + # Test case 2: Input just under the 128MB configured limit (should succeed) + # (2^25 - 32) elements * 4 bytes = 128MB - 128 bytes = 134,217,600 bytes + small_input = np.ones( + INCREASED_LIMIT_ELEMENTS - OFFSET_ELEMENTS, dtype=np.float32 + ) + input_bytes = small_input.tobytes() + assert ( + len(input_bytes) < 128 * MB + ) # Verify we're actually under the 128MB limit + + response = requests.post( + self._get_infer_url(model), data=input_bytes, headers=headers + ) + + # Should succeed with 200 OK + self.assertEqual( + 200, + response.status_code, + "Expected success code for request within increased limit, got: {}".format( + response.status_code + ), + ) + + # Verify output matches our input (identity model) + header_size = int(response.headers["Inference-Header-Content-Length"]) + output_data = response.content[header_size:] + + # Convert output bytes back to numpy array for comparison + output_array = np.frombuffer(output_data, dtype=np.float32) + self.assertTrue( + np.array_equal(output_array, small_input), + "Response data does not match input data", + ) + + def test_large_input_json(self): + """Test JSON input larger with custom limit set""" + model = "onnx_zero_1_float32" + + # Test case 1: Input just over the 128MB configured limit (should fail) + # (2^25 + 32) elements * 4 bytes = 128MB + 128 bytes = 134,217,856 bytes + shape_size = INCREASED_LIMIT_ELEMENTS + OFFSET_ELEMENTS + + payload = { + "inputs": [ + { + "name": "INPUT0", + "datatype": "FP32", + "shape": [1, shape_size], + "data": [1.0] * shape_size, + } + ] + } + assert ( + shape_size * BYTES_PER_FP32 > 128 * MB + ) # Verify we're actually over the 128MB limit + + headers = {"Content-Type": "application/json"} + response = requests.post( + self._get_infer_url(model), headers=headers, json=payload + ) + + # Should fail with 400 bad request with our increased limit + self.assertEqual( + 400, + response.status_code, + "Expected error code for oversized JSON request, got: {}".format( + response.status_code + ), + ) + + # Verify error message contains size limit info + error_msg = response.content.decode() + self.assertIn( + "exceeds the maximum allowed value", + error_msg, + "Expected error message about exceeding max input size", + ) + + # Test case 2: Input just under the 128MB configured limit (should succeed) + # (2^25 - 32) elements * 4 bytes = 128MB - 128 bytes = 134,217,600 bytes + shape_size = INCREASED_LIMIT_ELEMENTS - OFFSET_ELEMENTS + + payload = { + "inputs": [ + { + "name": "INPUT0", + "datatype": "FP32", + "shape": [1, shape_size], + "data": [1.0] * shape_size, + } + ] + } + assert ( + shape_size * BYTES_PER_FP32 < 128 * MB + ) # Verify we're actually under the 128MB limit + + response = requests.post( + self._get_infer_url(model), headers=headers, json=payload + ) + + # Should succeed with 200 OK + self.assertEqual( + 200, + response.status_code, + "Expected success code for request within increased limit, got: {}".format( + response.status_code + ), + ) + + # Verify we got a valid response + result = response.json() + self.assertIn("outputs", result, "Response missing outputs field") + self.assertEqual(1, len(result["outputs"]), "Expected 1 output") + self.assertEqual( + shape_size, + result["outputs"][0]["shape"][1], + f"Expected shape {[1, shape_size]}, got {result['outputs'][0]['shape']}", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/qa/L0_http/test.sh b/qa/L0_http/test.sh index 3c3df56e65..a96bd6a1c6 100755 --- a/qa/L0_http/test.sh +++ b/qa/L0_http/test.sh @@ -753,6 +753,108 @@ set -e kill $SERVER_PID wait $SERVER_PID +### Test HTTP input size limits ### + +# Setup models needed for the test +MODELDIR=http_input_size_limit_test_models +mkdir -p $MODELDIR +rm -rf ${MODELDIR}/* +cp -r $DATADIR/qa_identity_model_repository/onnx_zero_1_float32 ${MODELDIR}/. + +# First run with default size limit - large inputs should fail +SERVER_ARGS="--model-repository=${MODELDIR}" +SERVER_LOG="./inference_server_default_limit.log" +CLIENT_LOG="./http_input_size_limit_default.log" +run_server +if [ "$SERVER_PID" == "0" ]; then + echo -e "\n***\n*** Failed to start $SERVER\n***" + cat $SERVER_LOG + exit 1 +fi + +set +e +# Run test to verify that large inputs fail with default limit +python http_input_size_limit_test.py InferSizeLimitTest.test_default_limit_rejection_raw_binary >> $CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Default Input Size Limit Test Failed for raw binary input\n***" + RET=1 +fi + +python http_input_size_limit_test.py InferSizeLimitTest.test_default_limit_rejection_json >> $CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Default Input Size Limit Test Failed for JSON input\n***" + RET=1 +fi +set -e + +kill $SERVER_PID +wait $SERVER_PID + +# Now run with increased size limit (128MB) - large inputs should succeed +SERVER_ARGS="--model-repository=${MODELDIR} --http-max-input-size=$((2**27))" +SERVER_LOG="./inference_server_increased_limit.log" +CLIENT_LOG="./http_input_size_limit_increased.log" +run_server +if [ "$SERVER_PID" == "0" ]; then + echo -e "\n***\n*** Failed to start $SERVER with increased HTTP input size limit\n***" + cat $SERVER_LOG + exit 1 +fi + +rm -f $CLIENT_LOG +set +e +python http_input_size_limit_test.py InferSizeLimitTest.test_large_input_raw_binary >> $CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Input Size Limit Test Failed for raw binary input with increased limits\n***" + RET=1 +fi + +python http_input_size_limit_test.py InferSizeLimitTest.test_large_input_json >> $CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Input Size Limit Test Failed for JSON input with increased limits\n***" + RET=1 +fi +set -e + +kill $SERVER_PID +wait $SERVER_PID + +# Test with zero max input size - should fail to start +SERVER_ARGS="--model-repository=${MODELDIR} --http-max-input-size=0" +SERVER_LOG="./inference_server_zero_limit.log" +CLIENT_LOG="./http_input_size_limit_zero.log" +run_server +if [ "$SERVER_PID" != "0" ]; then + echo -e "\n***\n*** Server should not start with zero max input size\n***" + kill $SERVER_PID + wait $SERVER_PID + RET=1 +elif [ `grep -c "Error: --http-max-input-size must be greater than 0." ${SERVER_LOG}` != "1" ]; then + echo -e "\n***\n*** Failed. Expected '--http-max-input-size must be greater than 0' to be found in log\n***" + cat $SERVER_LOG + RET=1 +fi + +# Test with negative max input size - should fail to start +SERVER_ARGS="--model-repository=${MODELDIR} --http-max-input-size=-1024" +SERVER_LOG="./inference_server_negative_limit.log" +CLIENT_LOG="./http_input_size_limit_negative.log" +run_server +if [ "$SERVER_PID" != "0" ]; then + echo -e "\n***\n*** Server should not start with negative max input size\n***" + kill $SERVER_PID + wait $SERVER_PID + RET=1 +elif [ `grep -c "Error: --http-max-input-size must be greater than 0." ${SERVER_LOG}` != "1" ]; then + echo -e "\n***\n*** Failed. Expected '--http-max-input-size must be greater than 0' to be found in log\n***" + cat $SERVER_LOG + RET=1 +fi + ### if [ $RET -eq 0 ]; then diff --git a/src/command_line_parser.cc b/src/command_line_parser.cc index 577f6a50d5..633eb5f980 100644 --- a/src/command_line_parser.cc +++ b/src/command_line_parser.cc @@ -298,6 +298,7 @@ enum TritonOptionId { OPTION_HTTP_ADDRESS, OPTION_HTTP_THREAD_COUNT, OPTION_HTTP_RESTRICTED_API, + OPTION_HTTP_MAX_INPUT_SIZE, #endif // TRITON_ENABLE_HTTP #if defined(TRITON_ENABLE_GRPC) OPTION_ALLOW_GRPC, @@ -497,6 +498,10 @@ TritonParser::SetupOptions() http_options_.push_back( {OPTION_HTTP_THREAD_COUNT, "http-thread-count", Option::ArgInt, "Number of threads handling HTTP requests."}); + http_options_.push_back( + {OPTION_HTTP_MAX_INPUT_SIZE, "http-max-input-size", Option::ArgInt, + ("Maximum allowed HTTP request input size in bytes. Default is " + + std::to_string(HTTP_DEFAULT_MAX_INPUT_SIZE) + " bytes (64MB).")}); http_options_.push_back( {OPTION_HTTP_RESTRICTED_API, "http-restricted-api", ":=", @@ -1393,6 +1398,15 @@ TritonParser::Parse(int argc, char** argv) case OPTION_HTTP_THREAD_COUNT: lparams.http_thread_cnt_ = ParseOption(optarg); break; + case OPTION_HTTP_MAX_INPUT_SIZE: { + int64_t temp_input_size = ParseOption(optarg); + if (temp_input_size <= 0) { + throw ParseException( + "Error: --http-max-input-size must be greater than 0."); + } + lparams.http_max_input_size_ = temp_input_size; + break; + } case OPTION_HTTP_RESTRICTED_API: ParseRestrictedFeatureOption( optarg, long_options[option_index].name, "", "api", diff --git a/src/command_line_parser.h b/src/command_line_parser.h index 762ee87b6d..e6036fb969 100644 --- a/src/command_line_parser.h +++ b/src/command_line_parser.h @@ -1,4 +1,4 @@ -// Copyright 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -35,6 +35,7 @@ #include #include +#include "common.h" #include "restricted_features.h" #include "triton/common/logging.h" #include "triton/core/tritonserver.h" @@ -197,6 +198,8 @@ struct TritonServerParameters { // The number of threads to initialize for the HTTP front-end. int http_thread_cnt_{8}; RestrictedFeatures http_restricted_apis_{}; + // Default value 64MB + size_t http_max_input_size_{HTTP_DEFAULT_MAX_INPUT_SIZE}; #endif // TRITON_ENABLE_HTTP #ifdef TRITON_ENABLE_GRPC diff --git a/src/common.h b/src/common.h index 433b3bbf78..fe7e3be4cd 100644 --- a/src/common.h +++ b/src/common.h @@ -54,6 +54,9 @@ constexpr int WILDCARD_DIM = -1; // Maximum allowed depth for JSON parsing constexpr int32_t HTTP_MAX_JSON_NESTING_DEPTH = 100; +// Default maximum allowed HTTP request input size in bytes (64MB) +constexpr size_t HTTP_DEFAULT_MAX_INPUT_SIZE = 1 << 26; + /// Request parameter keys that start with a "triton_" prefix for internal use const std::vector TRITON_RESERVED_REQUEST_PARAMS{ "triton_enable_empty_final_response"}; diff --git a/src/http_server.cc b/src/http_server.cc index 8c2437d99c..e3ee3efe17 100644 --- a/src/http_server.cc +++ b/src/http_server.cc @@ -1162,7 +1162,7 @@ HTTPAPIServer::HTTPAPIServer( const std::shared_ptr& shm_manager, const int32_t port, const bool reuse_port, const std::string& address, const std::string& header_forward_pattern, const int thread_cnt, - const RestrictedFeatures& restricted_apis) + const size_t max_input_size, const RestrictedFeatures& restricted_apis) : HTTPServer(port, reuse_port, address, header_forward_pattern, thread_cnt), server_(server), trace_manager_(trace_manager), shm_manager_(shm_manager), allocator_(nullptr), server_regex_(R"(/v2(?:/health/(live|ready))?)"), @@ -1174,7 +1174,8 @@ HTTPAPIServer::HTTPAPIServer( R"(/v2/systemsharedmemory(?:/region/([^/]+))?/(status|register|unregister))"), cudasharedmemory_regex_( R"(/v2/cudasharedmemory(?:/region/([^/]+))?/(status|register|unregister))"), - trace_regex_(R"(/v2/trace/setting)"), restricted_apis_(restricted_apis) + trace_regex_(R"(/v2/trace/setting)"), max_input_size_(max_input_size), + restricted_apis_(restricted_apis) { // FIXME, don't cache server metadata. The http endpoint should // not be deciding that server metadata will not change during @@ -2808,6 +2809,19 @@ HTTPAPIServer::ParseJsonTritonIO( byte_size = element_cnt * type_byte_size; } + // Check if byte_size is larger than max_input_size_ + if (byte_size > max_input_size_) { + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + ("JSON input '" + std::string(input_name) + + "' has a byte_size (" + std::to_string(byte_size) + + " bytes) that exceeds the maximum allowed value " + "of " + + std::to_string(max_input_size_) + + " bytes. Use --http-max-input-size to increase the limit.") + .c_str()); + } + infer_req->serialized_data_.emplace_back(); std::vector& serialized = infer_req->serialized_data_.back(); serialized.resize(byte_size); @@ -3086,6 +3100,18 @@ HTTPAPIServer::EVBufferToRawInput( TRITONSERVER_InferenceRequestAddRawInput(irequest, raw_input_name)); size_t byte_size = evbuffer_get_length(input_buffer); + + // Check if byte_size is larger than max_input_size_ + if (byte_size > max_input_size_) { + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + ("Raw input has a byte_size (" + std::to_string(byte_size) + + " bytes) that exceeds the maximum allowed value of " + + std::to_string(max_input_size_) + + " bytes. Use --http-max-input-size to increase the limit.") + .c_str()); + } + // zero-shape tensor if (byte_size == 0) { RETURN_IF_ERR(TRITONSERVER_InferenceRequestAppendInputData( @@ -4795,12 +4821,12 @@ HTTPAPIServer::Create( const std::shared_ptr& shm_manager, const int32_t port, const bool reuse_port, const std::string& address, const std::string& header_forward_pattern, const int thread_cnt, - const RestrictedFeatures& restricted_features, + const size_t max_input_size, const RestrictedFeatures& restricted_features, std::unique_ptr* http_server) { http_server->reset(new HTTPAPIServer( server, trace_manager, shm_manager, port, reuse_port, address, - header_forward_pattern, thread_cnt, restricted_features)); + header_forward_pattern, thread_cnt, max_input_size, restricted_features)); const std::string addr = address + ":" + std::to_string(port); LOG_INFO << "Started HTTPService at " << addr; @@ -4833,7 +4859,8 @@ HTTPAPIServer::Create( return Create( server, trace_manager, shm_manager, port, reuse_port, address, - header_forward_pattern, thread_count, restricted_features, service); + header_forward_pattern, thread_count, HTTP_DEFAULT_MAX_INPUT_SIZE, + restricted_features, service); } diff --git a/src/http_server.h b/src/http_server.h index 785c408041..877327b4d5 100644 --- a/src/http_server.h +++ b/src/http_server.h @@ -198,7 +198,7 @@ class HTTPAPIServer : public HTTPServer { const std::shared_ptr& smb_manager, const int32_t port, const bool reuse_port, const std::string& address, const std::string& header_forward_pattern, const int thread_cnt, - const RestrictedFeatures& restricted_apis, + const size_t max_input_size, const RestrictedFeatures& restricted_apis, std::unique_ptr* http_server); static TRITONSERVER_Error* Create( @@ -500,6 +500,7 @@ class HTTPAPIServer : public HTTPServer { const std::shared_ptr& shm_manager, const int32_t port, const bool reuse_port, const std::string& address, const std::string& header_forward_pattern, const int thread_cnt, + const size_t max_input_size = HTTP_DEFAULT_MAX_INPUT_SIZE, const RestrictedFeatures& restricted_apis = {}); virtual void Handle(evhtp_request_t* req) override; @@ -684,6 +685,7 @@ class HTTPAPIServer : public HTTPServer { parameters_field, new MappingSchema(MappingSchema::Kind::MAPPING_SCHEMA, true)); } + size_t max_input_size_; RestrictedFeatures restricted_apis_{}; bool RespondIfRestricted( evhtp_request_t* req, const Restriction& restriction); diff --git a/src/main.cc b/src/main.cc index db558d45bf..6be2ff5e58 100644 --- a/src/main.cc +++ b/src/main.cc @@ -138,8 +138,8 @@ StartHttpService( server, trace_manager, shm_manager, g_triton_params.http_port_, g_triton_params.reuse_http_port_, g_triton_params.http_address_, g_triton_params.http_forward_header_pattern_, - g_triton_params.http_thread_cnt_, g_triton_params.http_restricted_apis_, - service); + g_triton_params.http_thread_cnt_, g_triton_params.http_max_input_size_, + g_triton_params.http_restricted_apis_, service); if (err == nullptr) { err = (*service)->Start(); } From 13801551cf4c14b3133f12502c06b34e50bf92e5 Mon Sep 17 00:00:00 2001 From: richardhuo-nv Date: Mon, 9 Jun 2025 10:49:30 -0700 Subject: [PATCH 045/230] ci: fix the trtllm tests after the repo migration of trtllm backend (#8241) --- .../openai_frontend/engine/utils/triton.py | 2 +- qa/L0_orca/test.sh | 49 +++++++++++++++++-- qa/L0_perf_tensorrt_llm/test.sh | 8 +-- 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/python/openai/openai_frontend/engine/utils/triton.py b/python/openai/openai_frontend/engine/utils/triton.py index ed16c2c1b8..53f1af44d6 100644 --- a/python/openai/openai_frontend/engine/utils/triton.py +++ b/python/openai/openai_frontend/engine/utils/triton.py @@ -129,7 +129,7 @@ def _create_trtllm_inference_request( if request.presence_penalty is not None: inputs["presence_penalty"] = np.float32([[request.presence_penalty]]) if request.seed is not None: - inputs["random_seed"] = np.uint64([[request.seed]]) + inputs["seed"] = np.uint64([[request.seed]]) if request.temperature is not None: inputs["temperature"] = np.float32([[request.temperature]]) diff --git a/qa/L0_orca/test.sh b/qa/L0_orca/test.sh index 6069a75048..03f5118bd2 100755 --- a/qa/L0_orca/test.sh +++ b/qa/L0_orca/test.sh @@ -36,7 +36,7 @@ MODEL_NAME="gpt2_tensorrt_llm" NAME="tensorrt_llm_benchmarking_test" MODEL_REPOSITORY="$(pwd)/triton_model_repo" TENSORRTLLM_BACKEND_DIR="/workspace/tensorrtllm_backend" -GPT_DIR="$TENSORRTLLM_BACKEND_DIR/tensorrt_llm/examples/gpt" +GPT_DIR="$TENSORRTLLM_BACKEND_DIR/tensorrt_llm/examples/models/core/gpt" TOKENIZER_DIR="$GPT_DIR/gpt2" ENGINES_DIR="${BASE_DIR}/engines/inflight_batcher_llm/${NUM_GPUS}-gpu" TRITON_DIR=${TRITON_DIR:="/opt/tritonserver"} @@ -48,9 +48,52 @@ CLIENT_PY=${BASE_DIR}/orca_http_test.py CLIENT_LOG="${NAME}_orca_http_test.log" source ../common/util.sh +function clone_tensorrt_llm_backend_repo { + rm -rf $TENSORRTLLM_BACKEND_DIR && mkdir $TENSORRTLLM_BACKEND_DIR + apt-get update && apt-get install git-lfs -y --no-install-recommends + git clone --single-branch --depth=1 -b ${TENSORRTLLM_BACKEND_REPO_TAG} ${TRITON_REPO_ORG}/tensorrtllm_backend.git $TENSORRTLLM_BACKEND_DIR + cd $TENSORRTLLM_BACKEND_DIR && git lfs install && git submodule update --init --recursive +} + +function build_gpt2_base_model { + # Download weights from HuggingFace Transformers + cd ${GPT_DIR} && rm -rf gpt2 && git clone https://huggingface.co/gpt2-medium gpt2 && cd gpt2 + rm pytorch_model.bin model.safetensors + if ! wget -q https://huggingface.co/gpt2-medium/resolve/main/pytorch_model.bin; then + echo "Downloading pytorch_model.bin failed." + exit 1 + fi + cd ${GPT_DIR} + + # Convert weights from HF Tranformers to FT format + python3 convert_checkpoint.py --model_dir gpt2 --dtype float16 --tp_size ${NUM_GPUS} --output_dir "./c-model/gpt2/${NUM_GPUS}-gpu/" + cd ${BASE_DIR} +} + +function build_gpt2_tensorrt_engine { + # Build TensorRT engines + cd ${GPT_DIR} + trtllm-build --checkpoint_dir "./c-model/gpt2/${NUM_GPUS}-gpu/" \ + --gpt_attention_plugin float16 \ + --remove_input_padding enable \ + --paged_kv_cache enable \ + --gemm_plugin float16 \ + --workers "${NUM_GPUS}" \ + --output_dir "${ENGINES_DIR}" + + cd ${BASE_DIR} +} + +function replace_config_tags { + tag_to_replace="${1}" + new_value="${2}" + config_file_path="${3}" + sed -i "s|${tag_to_replace}|${new_value}|g" ${config_file_path} +} + function prepare_model_repository { rm -rf ${MODEL_REPOSITORY} && mkdir ${MODEL_REPOSITORY} - cp -r ${TENSORRTLLM_BACKEND_DIR}/all_models/inflight_batcher_llm/* ${MODEL_REPOSITORY} + cp -r ${TENSORRTLLM_BACKEND_DIR}/tensorrt_llm/triton_backend/all_models/inflight_batcher_llm/* ${MODEL_REPOSITORY} rm -rf ${MODEL_REPOSITORY}/tensorrt_llm_bls mv "${MODEL_REPOSITORY}/ensemble" "${MODEL_REPOSITORY}/${MODEL_NAME}" @@ -113,7 +156,7 @@ function wait_for_server_ready() { } function run_server { - python3 ${TENSORRTLLM_BACKEND_DIR}/scripts/launch_triton_server.py --world_size="${NUM_GPUS}" --model_repo="${MODEL_REPOSITORY}" >${SERVER_LOG} 2>&1 & + python3 ${TENSORRTLLM_BACKEND_DIR}/tensorrt_llm/triton_backend/scripts/launch_triton_server.py --world_size="${NUM_GPUS}" --model_repo="${MODEL_REPOSITORY}" >${SERVER_LOG} 2>&1 & sleep 2 # allow time to obtain the pid(s) # Read PIDs into an array, trimming whitespaces readarray -t SERVER_PID < <(pgrep "tritonserver") diff --git a/qa/L0_perf_tensorrt_llm/test.sh b/qa/L0_perf_tensorrt_llm/test.sh index e74b01e568..5cfb4ba595 100755 --- a/qa/L0_perf_tensorrt_llm/test.sh +++ b/qa/L0_perf_tensorrt_llm/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -35,7 +35,7 @@ MODEL_NAME="gpt2_tensorrt_llm" NAME="tensorrt_llm_benchmarking_test" MODEL_REPOSITORY="$(pwd)/triton_model_repo" TENSORRTLLM_BACKEND_DIR="/workspace/tensorrtllm_backend" -GPT_DIR="$TENSORRTLLM_BACKEND_DIR/tensorrt_llm/examples/gpt" +GPT_DIR="$TENSORRTLLM_BACKEND_DIR/tensorrt_llm/examples/models/core/gpt" TOKENIZER_DIR="$GPT_DIR/gpt2" ENGINES_DIR="${BASE_DIR}/engines/inflight_batcher_llm/${NUM_GPUS}-gpu" TRITON_DIR=${TRITON_DIR:="/opt/tritonserver"} @@ -133,7 +133,7 @@ function replace_config_tags { function prepare_model_repository { rm -rf ${MODEL_REPOSITORY} && mkdir ${MODEL_REPOSITORY} - cp -r ${TENSORRTLLM_BACKEND_DIR}/all_models/inflight_batcher_llm/* ${MODEL_REPOSITORY} + cp -r ${TENSORRTLLM_BACKEND_DIR}/tensorrt_llm/triton_backend/all_models/inflight_batcher_llm/* ${MODEL_REPOSITORY} rm -rf ${MODEL_REPOSITORY}/tensorrt_llm_bls mv "${MODEL_REPOSITORY}/ensemble" "${MODEL_REPOSITORY}/${MODEL_NAME}" @@ -189,7 +189,7 @@ function wait_for_server_ready() { } function run_server { - python3 ${TENSORRTLLM_BACKEND_DIR}/scripts/launch_triton_server.py --world_size="${NUM_GPUS}" --model_repo="${MODEL_REPOSITORY}" >${SERVER_LOG} 2>&1 & + python3 ${TENSORRTLLM_BACKEND_DIR}/tensorrt_llm/triton_backend/scripts/launch_triton_server.py --world_size="${NUM_GPUS}" --model_repo="${MODEL_REPOSITORY}" >${SERVER_LOG} 2>&1 & sleep 2 # allow time to obtain the pid(s) # Read PIDs into an array, trimming whitespaces readarray -t SERVER_PID < <(pgrep "tritonserver") From 41844940e1601f223c33f6c640d512641ff6be5e Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Wed, 11 Jun 2025 11:49:52 -0700 Subject: [PATCH 046/230] Update default branch to track development for 2.60.0 / 25.07 (#8243) --- TRITON_VERSION | 2 +- build.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/TRITON_VERSION b/TRITON_VERSION index 6d43c77ac3..7b28f253ee 100644 --- a/TRITON_VERSION +++ b/TRITON_VERSION @@ -1 +1 @@ -2.59.0dev +2.60.0dev diff --git a/build.py b/build.py index 150719ccc0..3da4ad89e3 100755 --- a/build.py +++ b/build.py @@ -71,8 +71,8 @@ # DEFAULT_TRITON_VERSION_MAP = { - "release_version": "2.58.0dev", - "triton_container_version": "25.05dev", + "release_version": "2.60.0dev", + "triton_container_version": "25.07dev", "upstream_container_version": "25.05", "ort_version": "1.22.0", "ort_openvino_version": "2025.1.0", From 30e20cb9084233e1d87267d7c5c6f8a15cc57a99 Mon Sep 17 00:00:00 2001 From: Sai Kiran Polisetty Date: Tue, 24 Jun 2025 22:09:51 +0530 Subject: [PATCH 047/230] fix: Support max_completion_tokens option in OpenAI frontend (#8226) --- python/openai/README.md | 33 ++--------- .../openai_frontend/engine/triton_engine.py | 12 +++- .../openai_frontend/engine/utils/triton.py | 39 ++++++++++++- python/openai/openai_frontend/main.py | 8 +++ .../openai/openai_frontend/schemas/openai.py | 22 +++---- python/openai/tests/test_chat_completions.py | 58 +++++++++++++++---- python/openai/tests/test_openai_client.py | 8 ++- python/openai/tests/test_tool_calling.py | 24 ++++---- python/openai/tests/utils.py | 12 +++- 9 files changed, 145 insertions(+), 71 deletions(-) diff --git a/python/openai/README.md b/python/openai/README.md index 51d8c5db72..3d770caa2e 100644 --- a/python/openai/README.md +++ b/python/openai/README.md @@ -216,7 +216,7 @@ completion = client.chat.completions.create( }, {"role": "user", "content": "What are LLMs?"}, ], - max_tokens=256, + max_completion_tokens=256, ) print(completion.choices[0].message.content) @@ -487,7 +487,7 @@ messages = [ ] tool_calls = client.chat.completions.create( - messages=messages, model=model, tools=tools, max_tokens=128 + messages=messages, model=model, tools=tools, max_completion_tokens=128 ) function_name = tool_calls.choices[0].message.tool_calls[0].function.name function_arguments = tool_calls.choices[0].message.tool_calls[0].function.arguments @@ -504,31 +504,6 @@ function arguments: {"city": "Dallas", "state": "TX", "unit": "fahrenheit"} tool calling result: The weather in Dallas, Texas is 85 degrees fahrenheit. It is partly cloudly, with highs in the 90's. ``` - -> [!WARNING] -> When using LangChain to call the `v1/chat/completions` endpoint, you might encounter an exception related to `max_completion_tokens` if you have specified `max_tokens` in the request. -> -> Example: `openai.BadRequestError: Error code: 400 - {'object': 'error', 'message': "[{'type': 'extra_forbidden', 'loc': ('body', 'max_completion_tokens'), 'msg': 'Extra inputs are not permitted', 'input': 800}]", 'type': 'BadRequestError', 'param': None, 'code': 400}` -> -> This issue is due to an incompatibility between Triton's OpenAI API frontend and the latest OpenAI API. We are actively working to address this gap. A workaround is adding the `max_tokens` into the `model_kwargs` of the LangChain OpenAI request. -> -> Example: -```python -from langchain.llms import OpenAI - -llm = OpenAI( - model_name="llama-3.1-8b-instruct", - temperature=0.0, - model_kwargs={ - "max_tokens": 4096 - } -) - -response = llm("Write a short poem about a sunset.") -print(response) - -``` - #### Named Tool Calling The OpenAI frontend supports named function calling, utilizing guided decoding in the vLLM and TensorRT-LLM backends. Users can specify one of the tools in `tool_choice` to force the model to select a specific tool for function calling. @@ -639,12 +614,12 @@ messages = [ ] tool_calls = client.chat.completions.create( - messages=messages, model=model, tools=tools, tool_choice=tool_choice, max_tokens=128 + messages=messages, model=model, tools=tools, tool_choice=tool_choice, max_completion_tokens=128 ) function_name = tool_calls.choices[0].message.tool_calls[0].function.name function_arguments = tool_calls.choices[0].message.tool_calls[0].function.arguments -print(f"function name: "{function_name}") +print(f"function name: {function_name}") print(f"function arguments: {function_arguments}") print(f"tool calling result: {available_tools[function_name](**json.loads(function_arguments))}") ``` diff --git a/python/openai/openai_frontend/engine/triton_engine.py b/python/openai/openai_frontend/engine/triton_engine.py index 32357cfdd4..c29faa288a 100644 --- a/python/openai/openai_frontend/engine/triton_engine.py +++ b/python/openai/openai_frontend/engine/triton_engine.py @@ -102,6 +102,7 @@ def __init__( self, server: tritonserver.Server, tokenizer: str, + default_max_tokens: int, backend: Optional[str] = None, lora_separator: Optional[str] = None, tool_call_parser: Optional[str] = None, @@ -113,6 +114,7 @@ def __init__( # TODO: Reconsider name of "backend" vs. something like "request_format" self.backend = backend self.lora_separator = lora_separator + self.default_max_tokens = default_max_tokens # NOTE: Creation time and model metadata will be static at startup for # now, and won't account for dynamically loading/unloading models. @@ -184,7 +186,9 @@ async def chat( # Convert to Triton request format and perform inference responses = metadata.model.async_infer( - metadata.request_converter(metadata.model, prompt, request, lora_name) + metadata.request_converter( + metadata.model, prompt, request, lora_name, self.default_max_tokens + ) ) # Prepare and send responses back to client in OpenAI format @@ -302,7 +306,11 @@ async def completion( # Convert to Triton request format and perform inference responses = metadata.model.async_infer( metadata.request_converter( - metadata.model, request.prompt, request, lora_name + metadata.model, + request.prompt, + request, + lora_name, + self.default_max_tokens, ) ) diff --git a/python/openai/openai_frontend/engine/utils/triton.py b/python/openai/openai_frontend/engine/utils/triton.py index 53f1af44d6..4e94a7b203 100644 --- a/python/openai/openai_frontend/engine/utils/triton.py +++ b/python/openai/openai_frontend/engine/utils/triton.py @@ -46,6 +46,7 @@ def _create_vllm_inference_request( prompt, request: CreateChatCompletionRequest | CreateCompletionRequest, lora_name: str | None, + default_max_tokens: int, ): inputs = {} # Exclude non-sampling parameters so they aren't passed to vLLM @@ -67,6 +68,9 @@ def _create_vllm_inference_request( "function_call", "functions", "suffix", + "max_completion_tokens", + # will be handled explicitly + "max_tokens", } # NOTE: The exclude_none is important, as internals may not support @@ -75,6 +79,23 @@ def _create_vllm_inference_request( exclude=excludes, exclude_none=True, ) + + # Indicates CreateChatCompletionRequest + if hasattr(request, "max_completion_tokens"): + if request.max_completion_tokens is not None: + sampling_parameters["max_tokens"] = request.max_completion_tokens + # Fallback to deprecated request.max_tokens + elif request.max_tokens is not None: + sampling_parameters["max_tokens"] = request.max_tokens + # If neither is set, use a default value for max_tokens + else: + sampling_parameters["max_tokens"] = default_max_tokens + # Indicates CreateCompletionRequest + elif request.max_tokens is not None: + sampling_parameters["max_tokens"] = request.max_tokens + else: + sampling_parameters["max_tokens"] = default_max_tokens + if lora_name is not None: sampling_parameters["lora_name"] = lora_name sampling_parameters = json.dumps(sampling_parameters) @@ -108,6 +129,7 @@ def _create_trtllm_inference_request( prompt, request: CreateChatCompletionRequest | CreateCompletionRequest, lora_name: str | None, + default_max_tokens: int, ): if lora_name is not None: raise Exception("LoRA selection is currently not supported for TRT-LLM backend") @@ -115,8 +137,23 @@ def _create_trtllm_inference_request( inputs = {} inputs["text_input"] = [[prompt]] inputs["stream"] = np.bool_([[request.stream]]) - if request.max_tokens: + + # Indicates CreateChatCompletionRequest + if hasattr(request, "max_completion_tokens"): + if request.max_completion_tokens is not None: + inputs["max_tokens"] = np.int32([[request.max_completion_tokens]]) + # Fallback to deprecated request.max_tokens + elif request.max_tokens is not None: + inputs["max_tokens"] = np.int32([[request.max_tokens]]) + # If neither is set, use a default value for max_tokens + else: + inputs["max_tokens"] = np.int32([[default_max_tokens]]) + # Indicates CreateCompletionRequest + elif request.max_tokens is not None: inputs["max_tokens"] = np.int32([[request.max_tokens]]) + else: + inputs["max_tokens"] = np.int32([[default_max_tokens]]) + if request.stop: if isinstance(request.stop, str): request.stop = [request.stop] diff --git a/python/openai/openai_frontend/main.py b/python/openai/openai_frontend/main.py index 8de1f8c2f2..779a0d868a 100755 --- a/python/openai/openai_frontend/main.py +++ b/python/openai/openai_frontend/main.py @@ -143,6 +143,13 @@ def parse_args(): help="The path to the custom Jinja chat template file. This is useful if you'd like to use a different chat template than the one provided by the model.", ) + triton_group.add_argument( + "--default-max-tokens", + type=int, + default=16, + help="The default maximum number of tokens to generate if not specified in the request. The default is 16.", + ) + # OpenAI-Compatible Frontend (FastAPI) openai_group = parser.add_argument_group("Triton OpenAI-Compatible Frontend") openai_group.add_argument( @@ -199,6 +206,7 @@ def main(): lora_separator=args.lora_separator, tool_call_parser=args.tool_call_parser, chat_template=args.chat_template, + default_max_tokens=args.default_max_tokens, ) # Attach TritonLLMEngine as the backbone for inference and model management diff --git a/python/openai/openai_frontend/schemas/openai.py b/python/openai/openai_frontend/schemas/openai.py index a2438e8394..86d75cb09d 100644 --- a/python/openai/openai_frontend/schemas/openai.py +++ b/python/openai/openai_frontend/schemas/openai.py @@ -103,7 +103,7 @@ class CreateCompletionRequest(BaseModel): description="Include the log probabilities on the `logprobs` most likely output tokens, as well the chosen tokens. For example, if `logprobs` is 5, the API will return a list of the 5 most likely tokens. The API will always return the `logprob` of the sampled token, so there may be up to `logprobs+1` elements in the response.\n\nThe maximum value for `logprobs` is 5.\n", ) max_tokens: Optional[conint(ge=0)] = Field( - 16, + None, description="The maximum number of [tokens](/tokenizer) that can be generated in the completion.\n\nThe token count of your prompt plus `max_tokens` cannot exceed the model's context length. [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) for counting tokens.\n", examples=[16], ) @@ -526,14 +526,6 @@ class Logprobs2(BaseModel): ) -class ChatCompletionFinishReason(Enum): - stop = "stop" - length = "length" - tool_calls = "tool_calls" - content_filter = "content_filter" - function_call = "function_call" - - class ChatCompletionStreamingResponseChoice(BaseModel): delta: ChatCompletionStreamResponseDelta logprobs: Optional[Logprobs2] = Field( @@ -850,11 +842,15 @@ class CreateChatCompletionRequest(BaseModel): None, description="An integer between 0 and 20 specifying the number of most likely tokens to return at each token position, each with an associated log probability. `logprobs` must be set to `true` if this parameter is used.", ) - # TODO: Consider new max_completion_tokens field in the future: https://platform.openai.com/docs/api-reference/chat/create#chat-create-max_completion_tokens - max_tokens: Optional[conint(ge=0)] = Field( - 16, + max_completion_tokens: Optional[conint(ge=0)] = Field( + None, description="The maximum number of [tokens](/tokenizer) that can be generated in the chat completion.\n\nThe total length of input tokens and generated tokens is limited by the model's context length. [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) for counting tokens.\n", ) + # TODO: Remove support for max_tokens field in the future: https://platform.openai.com/docs/api-reference/chat/create#chat-create-max_completion_tokens + max_tokens: Optional[conint(ge=0)] = Field( + None, + description="DEPRECATED: Use `max_completion_tokens` instead. The maximum number of [tokens](/tokenizer) that can be generated in the chat completion.\n\nThe total length of input tokens and generated tokens is limited by the model's context length. [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) for counting tokens.\n", + ) # TODO: Extension, flesh out description and defaults min_tokens: Optional[conint(ge=0)] = Field( None, @@ -871,7 +867,7 @@ class CreateChatCompletionRequest(BaseModel): ) response_format: Optional[ResponseFormat] = Field( None, - description='An object specifying the format that the model must output. Compatible with [GPT-4 Turbo](/docs/models/gpt-4-and-gpt-4-turbo) and all GPT-3.5 Turbo models newer than `gpt-3.5-turbo-1106`.\n\nSetting to `{ "type": "json_object" }` enables JSON mode, which guarantees the message the model generates is valid JSON.\n\n**Important:** when using JSON mode, you **must** also instruct the model to produce JSON yourself via a system or user message. Without this, the model may generate an unending stream of whitespace until the generation reaches the token limit, resulting in a long-running and seemingly "stuck" request. Also note that the message content may be partially cut off if `finish_reason="length"`, which indicates the generation exceeded `max_tokens` or the conversation exceeded the max context length.\n', + description='An object specifying the format that the model must output. Compatible with [GPT-4 Turbo](/docs/models/gpt-4-and-gpt-4-turbo) and all GPT-3.5 Turbo models newer than `gpt-3.5-turbo-1106`.\n\nSetting to `{ "type": "json_object" }` enables JSON mode, which guarantees the message the model generates is valid JSON.\n\n**Important:** when using JSON mode, you **must** also instruct the model to produce JSON yourself via a system or user message. Without this, the model may generate an unending stream of whitespace until the generation reaches the token limit, resulting in a long-running and seemingly "stuck" request. Also note that the message content may be partially cut off if `finish_reason="length"`, which indicates the generation exceeded `max_completion_tokens` or the conversation exceeded the max context length.\n', ) seed: Optional[conint(ge=-9223372036854775808, le=9223372036854775807)] = Field( None, diff --git a/python/openai/tests/test_chat_completions.py b/python/openai/tests/test_chat_completions.py index 532d898788..ccead85e70 100644 --- a/python/openai/tests/test_chat_completions.py +++ b/python/openai/tests/test_chat_completions.py @@ -123,6 +123,7 @@ def test_chat_completions_user_prompt_dict(self, client, model: str): [ ("temperature", 0.7), ("max_tokens", 10), + ("max_completion_tokens", 10), ("top_p", 0.9), ("frequency_penalty", 0.5), ("presence_penalty", 0.2), @@ -172,6 +173,7 @@ def test_chat_completions_sampling_parameters( ("temperature", 2.1), ("temperature", -0.1), ("max_tokens", -1), + ("max_completion_tokens", -1), ("top_p", 1.1), ("frequency_penalty", 3), ("frequency_penalty", -3), @@ -199,14 +201,21 @@ def test_chat_completions_invalid_sampling_parameters( assert response.status_code == 422 # Simple tests to verify max_tokens roughly behaves as expected + @pytest.mark.parametrize( + "max_tokens_key", + [ + "max_tokens", + "max_completion_tokens", + ], + ) def test_chat_completions_max_tokens( - self, client, model: str, messages: List[dict] + self, client, max_tokens_key, model: str, messages: List[dict] ): responses = [] - payload = {"model": model, "messages": messages, "max_tokens": 1} + payload = {"model": model, "messages": messages} - # Send two requests with max_tokens = 1 to check their similarity - payload["max_tokens"] = 1 + # Send two requests with max_tokens/max_completion_tokens = 1 to check their similarity + payload[max_tokens_key] = 1 responses.append( client.post( "/v1/chat/completions", @@ -219,8 +228,8 @@ def test_chat_completions_max_tokens( json=payload, ) ) - # Send one requests with larger max_tokens to check its dis-similarity - payload["max_tokens"] = 100 + # Send one requests with larger max_tokens/max_completion_tokens to check its dis-similarity + payload[max_tokens_key] = 100 responses.append( client.post( "/v1/chat/completions", @@ -245,6 +254,30 @@ def test_chat_completions_max_tokens( assert len(response1_text) == len(response2_text) == 1 assert len(response3_text) > len(response1_text) + def test_chat_completions_max_completion_tokens_precedence( + self, client, model: str, messages: List[dict] + ): + payload = { + "model": model, + "messages": messages, + "max_tokens": 50, # Higher value for max_tokens + "max_completion_tokens": 1, # Lower, expected to take precedence + } + + response = client.post( + "/v1/chat/completions", + json=payload, + ) + + print("Response:", response.json()) + assert response.status_code == 200 + + response_text_words = ( + response.json()["choices"][0]["message"]["content"].strip().split() + ) + # Check if the number of words is around max_completion_tokens + assert len(response_text_words) == 1 + @pytest.mark.parametrize( "temperature", [0.0, 1.0], @@ -260,7 +293,7 @@ def test_chat_completions_temperature_vllm( payload = { "model": model, "messages": messages, - "max_tokens": 256, + "max_completion_tokens": 256, "temperature": temperature, } @@ -321,7 +354,7 @@ def test_chat_completions_temperature_tensorrtllm( "model": model, "messages": messages, # Increase token length to allow more room for variability - "max_tokens": 200, + "max_completion_tokens": 200, "temperature": 0.0, # TRT-LLM requires certain settings of `top_k` / `top_p` to # respect changes in `temperature` @@ -376,7 +409,7 @@ def test_chat_completions_seed(self, client, model: str, messages: List[dict]): "model": model, "messages": messages, # Increase token length to allow more room for variability - "max_tokens": 200, + "max_completion_tokens": 200, "seed": 1, } payload2 = copy.deepcopy(payload1) @@ -559,7 +592,12 @@ def test_chat_completions_custom_tokenizer( responses = [] with TestClient(app_local) as client_local, TestClient(app_hf) as client_hf: - payload = {"model": model, "messages": messages, "temperature": 0} + payload = { + "model": model, + "messages": messages, + "temperature": 0, + "seed": 0, + } responses.append(client_local.post("/v1/chat/completions", json=payload)) responses.append(client_hf.post("/v1/chat/completions", json=payload)) diff --git a/python/openai/tests/test_openai_client.py b/python/openai/tests/test_openai_client.py index 618e4052ed..efcc31f93e 100644 --- a/python/openai/tests/test_openai_client.py +++ b/python/openai/tests/test_openai_client.py @@ -152,6 +152,7 @@ async def test_completion_streaming( max_tokens=10, temperature=0.0, stream=False, + seed=0, ) output = chat_completion.choices[0].text stop_reason = chat_completion.choices[0].finish_reason @@ -163,6 +164,7 @@ async def test_completion_streaming( max_tokens=10, temperature=0.0, stream=True, + seed=0, ) chunks = [] finish_reason_count = 0 @@ -200,13 +202,13 @@ async def test_chat_streaming( seed = 0 temperature = 0.0 # Generate enough tokens to easily identify stop words are working. - max_tokens = 64 + max_completion_tokens = 64 # Test single chat completion for comparison chat_completion = await client.chat.completions.create( model=model, messages=messages, - max_tokens=max_tokens, + max_completion_tokens=max_completion_tokens, temperature=temperature, seed=seed, stream=False, @@ -219,7 +221,7 @@ async def test_chat_streaming( stream = await client.chat.completions.create( model=model, messages=messages, - max_tokens=max_tokens, + max_completion_tokens=max_completion_tokens, temperature=temperature, seed=seed, stream=True, diff --git a/python/openai/tests/test_tool_calling.py b/python/openai/tests/test_tool_calling.py index 9299e4a036..c738b96303 100644 --- a/python/openai/tests/test_tool_calling.py +++ b/python/openai/tests/test_tool_calling.py @@ -179,7 +179,7 @@ async def test_tool_call_and_choice(self, client: openai.AsyncOpenAI, model: str chat_completion = await client.chat.completions.create( messages=MESSAGES_ASKING_FOR_TOOLS, temperature=0, - max_tokens=128, + max_completion_tokens=128, model=model, tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], logprobs=False, @@ -213,7 +213,7 @@ async def test_tool_call_and_choice(self, client: openai.AsyncOpenAI, model: str model=model, messages=MESSAGES_ASKING_FOR_TOOLS, temperature=0, - max_tokens=128, + max_completion_tokens=128, tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], logprobs=False, stream=True, @@ -288,10 +288,11 @@ async def test_tool_call_with_reply_response( chat_completion = await client.chat.completions.create( messages=MESSAGES_WITH_TOOL_RESPONSE, temperature=0, - max_tokens=128, + max_completion_tokens=128, model=model, tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], logprobs=False, + seed=0, ) choice = chat_completion.choices[0] @@ -304,11 +305,12 @@ async def test_tool_call_with_reply_response( stream = await client.chat.completions.create( messages=MESSAGES_WITH_TOOL_RESPONSE, temperature=0, - max_tokens=128, + max_completion_tokens=128, model=model, tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], logprobs=False, stream=True, + seed=0, ) chunks: List[str] = [] @@ -350,7 +352,7 @@ async def test_tool_call_with_named_tool_choice( chat_completion = await client.chat.completions.create( messages=MESSAGES_ASKING_FOR_TOOLS, temperature=0, - max_tokens=128, + max_completion_tokens=128, model=model, tool_choice=WEATHER_FORECAST_TOOL_CHOICE, tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], @@ -385,7 +387,7 @@ async def test_tool_call_with_named_tool_choice( model=model, messages=MESSAGES_ASKING_FOR_TOOLS, temperature=0, - max_tokens=128, + max_completion_tokens=128, tool_choice=WEATHER_FORECAST_TOOL_CHOICE, tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], logprobs=False, @@ -457,7 +459,7 @@ async def test_tool_call_with_required_tool_choice( chat_completion = await client.chat.completions.create( messages=MESSAGES_ASKING_FOR_TOOLS, temperature=0, - max_tokens=128, + max_completion_tokens=128, model=model, tool_choice="required", tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], @@ -492,7 +494,7 @@ async def test_tool_call_with_required_tool_choice( model=model, messages=MESSAGES_ASKING_FOR_TOOLS, temperature=0, - max_tokens=128, + max_completion_tokens=128, tool_choice="required", tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], logprobs=False, @@ -562,7 +564,7 @@ async def test_inconsistent_tool_choice_and_tools( await client.chat.completions.create( messages=MESSAGES_ASKING_FOR_TOOLS, temperature=0, - max_tokens=128, + max_completion_tokens=128, model=model, tool_choice=WEATHER_FORECAST_TOOL_CHOICE, logprobs=False, @@ -572,7 +574,7 @@ async def test_inconsistent_tool_choice_and_tools( await client.chat.completions.create( messages=MESSAGES_ASKING_FOR_TOOLS, temperature=0, - max_tokens=128, + max_completion_tokens=128, model=model, tool_choice=WEATHER_FORECAST_TOOL_CHOICE, tools=[WEATHER_TOOL], @@ -584,7 +586,7 @@ async def test_inconsistent_tool_choice_and_tools( await client.chat.completions.create( messages=MESSAGES_ASKING_FOR_TOOLS, temperature=0, - max_tokens=128, + max_completion_tokens=128, model=model, tool_choice="required", tools=[], diff --git a/python/openai/tests/utils.py b/python/openai/tests/utils.py index 59990ce2a1..4bacca6f99 100644 --- a/python/openai/tests/utils.py +++ b/python/openai/tests/utils.py @@ -52,9 +52,17 @@ def setup_server(model_repository: str): return server -def setup_fastapi_app(tokenizer: str, server: tritonserver.Server, backend: str): +def setup_fastapi_app( + tokenizer: str, + server: tritonserver.Server, + backend: str, + default_max_tokens: int = 16, +): engine: TritonLLMEngine = TritonLLMEngine( - server=server, tokenizer=tokenizer, backend=backend + server=server, + tokenizer=tokenizer, + backend=backend, + default_max_tokens=default_max_tokens, ) frontend: FastApiFrontend = FastApiFrontend(engine=engine) return frontend.app From 152eb3190c9b05c365e2fc3956823f930c1482f9 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Sat, 28 Jun 2025 08:57:07 -0700 Subject: [PATCH 048/230] post-25.06: Update default branch (#8271) Co-authored-by: Yingge He <157551214+yinggeh@users.noreply.github.com> --- Dockerfile.sdk | 2 +- README.md | 10 ++++---- build.py | 4 +-- deploy/aws/values.yaml | 2 +- deploy/fleetcommand/Chart.yaml | 2 +- deploy/fleetcommand/values.yaml | 6 ++--- deploy/gcp/values.yaml | 2 +- .../perf-analyzer-script/triton_client.yaml | 2 +- .../server-deployer/build_and_push.sh | 6 ++--- .../server-deployer/chart/triton/Chart.yaml | 4 +-- .../server-deployer/chart/triton/values.yaml | 6 ++--- .../server-deployer/data-test/schema.yaml | 2 +- .../server-deployer/schema.yaml | 4 +-- .../gke-marketplace-app/trt-engine/README.md | 6 ++--- deploy/k8s-onprem/values.yaml | 2 +- deploy/oci/values.yaml | 2 +- docs/introduction/compatibility.md | 25 ++++++++++--------- docs/introduction/release_notes.md | 4 +-- python/openai/README.md | 2 +- qa/L0_client_nobatch/test.sh | 2 +- qa/L0_long_running_stress/scenarios.py | 10 ++++---- qa/L0_long_running_stress/test.sh | 5 ++-- qa/common/gen_jetson_trt_models | 2 +- qa/common/gen_qa_custom_ops | 2 +- qa/common/gen_qa_model_repository | 2 +- 25 files changed, 58 insertions(+), 58 deletions(-) diff --git a/Dockerfile.sdk b/Dockerfile.sdk index 33414c3fe4..d1c4b5a189 100644 --- a/Dockerfile.sdk +++ b/Dockerfile.sdk @@ -29,7 +29,7 @@ # # Base image on the minimum Triton container -ARG BASE_IMAGE=nvcr.io/nvidia/tritonserver:25.05-py3-min +ARG BASE_IMAGE=nvcr.io/nvidia/tritonserver:25.06-py3-min ARG TRITON_CLIENT_REPO_SUBDIR=clientrepo ARG TRITON_PA_REPO_SUBDIR=perfanalyzerrepo diff --git a/README.md b/README.md index 2de74ae96a..057e58e8f9 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ >[!WARNING] >You are currently on the `main` branch which tracks under-development progress ->towards the next release. The current release is version [2.58.0](https://github.com/triton-inference-server/server/releases/latest) ->and corresponds to the 25.05 container release on NVIDIA GPU Cloud (NGC). +>towards the next release. The current release is version [2.59.0](https://github.com/triton-inference-server/server/releases/latest) +>and corresponds to the 25.06 container release on NVIDIA GPU Cloud (NGC). # Triton Inference Server @@ -90,16 +90,16 @@ Inference Server with the ```bash # Step 1: Create the example model repository -git clone -b r25.05 https://github.com/triton-inference-server/server.git +git clone -b r25.06 https://github.com/triton-inference-server/server.git cd server/docs/examples ./fetch_models.sh # Step 2: Launch triton from the NGC Triton container -docker run --gpus=1 --rm --net=host -v ${PWD}/model_repository:/models nvcr.io/nvidia/tritonserver:25.05-py3 tritonserver --model-repository=/models --model-control-mode explicit --load-model densenet_onnx +docker run --gpus=1 --rm --net=host -v ${PWD}/model_repository:/models nvcr.io/nvidia/tritonserver:25.06-py3 tritonserver --model-repository=/models --model-control-mode explicit --load-model densenet_onnx # Step 3: Sending an Inference Request # In a separate console, launch the image_client example from the NGC Triton SDK container -docker run -it --rm --net=host nvcr.io/nvidia/tritonserver:25.05-py3-sdk /workspace/install/bin/image_client -m densenet_onnx -c 3 -s INCEPTION /workspace/images/mug.jpg +docker run -it --rm --net=host nvcr.io/nvidia/tritonserver:25.06-py3-sdk /workspace/install/bin/image_client -m densenet_onnx -c 3 -s INCEPTION /workspace/images/mug.jpg # Inference should return the following Image '/workspace/images/mug.jpg': diff --git a/build.py b/build.py index 3da4ad89e3..ff069c7d20 100755 --- a/build.py +++ b/build.py @@ -73,12 +73,12 @@ DEFAULT_TRITON_VERSION_MAP = { "release_version": "2.60.0dev", "triton_container_version": "25.07dev", - "upstream_container_version": "25.05", + "upstream_container_version": "25.06", "ort_version": "1.22.0", "ort_openvino_version": "2025.1.0", "standalone_openvino_version": "2025.1.0", "dcgm_version": "3.3.6", - "vllm_version": "0.8.4", + "vllm_version": "0.9.0.1", "rhel_py_version": "3.12.3", } diff --git a/deploy/aws/values.yaml b/deploy/aws/values.yaml index cddaaaf3a6..c24b0ed7b8 100644 --- a/deploy/aws/values.yaml +++ b/deploy/aws/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.05-py3 + imageName: nvcr.io/nvidia/tritonserver:25.06-py3 pullPolicy: IfNotPresent modelRepositoryPath: s3://triton-inference-server-repository/model_repository numGpus: 1 diff --git a/deploy/fleetcommand/Chart.yaml b/deploy/fleetcommand/Chart.yaml index 0664a5d959..b819cbb4a5 100644 --- a/deploy/fleetcommand/Chart.yaml +++ b/deploy/fleetcommand/Chart.yaml @@ -26,7 +26,7 @@ apiVersion: v1 # appVersion is the Triton version; update when changing release -appVersion: "2.58.0" +appVersion: "2.59.0" description: Triton Inference Server (Fleet Command) name: triton-inference-server # version is the Chart version; update when changing anything in the chart diff --git a/deploy/fleetcommand/values.yaml b/deploy/fleetcommand/values.yaml index b4044b795a..d2300ef421 100644 --- a/deploy/fleetcommand/values.yaml +++ b/deploy/fleetcommand/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.05-py3 + imageName: nvcr.io/nvidia/tritonserver:25.06-py3 pullPolicy: IfNotPresent numGpus: 1 serverCommand: tritonserver @@ -47,13 +47,13 @@ image: # # To set model control mode, uncomment and configure below # TODO: Fix the following url, it is invalid - # See https://github.com/triton-inference-server/server/blob/r25.05/docs/user_guide/model_management.md + # See https://github.com/triton-inference-server/server/blob/r25.06/docs/user_guide/model_management.md # for more details #- --model-control-mode=explicit|poll|none # # Additional server args # - # see https://github.com/triton-inference-server/server/blob/r25.05/README.md + # see https://github.com/triton-inference-server/server/blob/r25.06/README.md # for more details service: diff --git a/deploy/gcp/values.yaml b/deploy/gcp/values.yaml index 3770934cc8..73948fb39d 100644 --- a/deploy/gcp/values.yaml +++ b/deploy/gcp/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.05-py3 + imageName: nvcr.io/nvidia/tritonserver:25.06-py3 pullPolicy: IfNotPresent modelRepositoryPath: gs://triton-inference-server-repository/model_repository numGpus: 1 diff --git a/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml b/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml index 38a65ed8e8..8be23e0f2c 100644 --- a/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml +++ b/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml @@ -33,7 +33,7 @@ metadata: namespace: default spec: containers: - - image: nvcr.io/nvidia/tritonserver:25.05-py3-sdk + - image: nvcr.io/nvidia/tritonserver:25.06-py3-sdk imagePullPolicy: Always name: nv-triton-client securityContext: diff --git a/deploy/gke-marketplace-app/server-deployer/build_and_push.sh b/deploy/gke-marketplace-app/server-deployer/build_and_push.sh index 6cb6f76e0a..b31804b9a2 100755 --- a/deploy/gke-marketplace-app/server-deployer/build_and_push.sh +++ b/deploy/gke-marketplace-app/server-deployer/build_and_push.sh @@ -27,9 +27,9 @@ export REGISTRY=gcr.io/$(gcloud config get-value project | tr ':' '/') export APP_NAME=tritonserver -export MAJOR_VERSION=2.58 -export MINOR_VERSION=2.58.0 -export NGC_VERSION=25.05-py3 +export MAJOR_VERSION=2.59 +export MINOR_VERSION=2.59.0 +export NGC_VERSION=25.06-py3 docker pull nvcr.io/nvidia/$APP_NAME:$NGC_VERSION diff --git a/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml b/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml index 5011733bbc..3206248935 100644 --- a/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml +++ b/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml @@ -25,7 +25,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. apiVersion: v1 -appVersion: "2.58" +appVersion: "2.59" description: Triton Inference Server name: triton-inference-server -version: 2.58.0 +version: 2.59.0 diff --git a/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml b/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml index ed69180192..52b1ab2f21 100644 --- a/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml +++ b/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml @@ -31,14 +31,14 @@ maxReplicaCount: 3 tritonProtocol: HTTP # HPA GPU utilization autoscaling target HPATargetAverageValue: 85 -modelRepositoryPath: gs://triton_sample_models/25.05 -publishedVersion: '2.58.0' +modelRepositoryPath: gs://triton_sample_models/25.06 +publishedVersion: '2.59.0' gcpMarketplace: true image: registry: gcr.io repository: nvidia-ngc-public/tritonserver - tag: 25.05-py3 + tag: 25.06-py3 pullPolicy: IfNotPresent # modify the model repository here to match your GCP storage bucket numGpus: 1 diff --git a/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml b/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml index 2c55441213..36ba6b6a63 100644 --- a/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml +++ b/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml @@ -27,7 +27,7 @@ x-google-marketplace: schemaVersion: v2 applicationApiVersion: v1beta1 - publishedVersion: '2.58.0' + publishedVersion: '2.59.0' publishedVersionMetadata: releaseNote: >- Initial release. diff --git a/deploy/gke-marketplace-app/server-deployer/schema.yaml b/deploy/gke-marketplace-app/server-deployer/schema.yaml index 68c171d28a..629303952d 100644 --- a/deploy/gke-marketplace-app/server-deployer/schema.yaml +++ b/deploy/gke-marketplace-app/server-deployer/schema.yaml @@ -27,7 +27,7 @@ x-google-marketplace: schemaVersion: v2 applicationApiVersion: v1beta1 - publishedVersion: '2.58.0' + publishedVersion: '2.59.0' publishedVersionMetadata: releaseNote: >- Initial release. @@ -89,7 +89,7 @@ properties: modelRepositoryPath: type: string title: Bucket where models are stored. Please make sure the user/service account to create the GKE app has permission to this GCS bucket. Read Triton documentation on configs and formatting details, supporting TensorRT, TensorFlow, Pytorch, Onnx ... etc. - default: gs://triton_sample_models/25.05 + default: gs://triton_sample_models/25.06 image.ldPreloadPath: type: string title: Leave this empty by default. Triton allows users to create custom layers for backend such as TensorRT plugin or Tensorflow custom ops, the compiled shared library must be provided via LD_PRELOAD environment variable. diff --git a/deploy/gke-marketplace-app/trt-engine/README.md b/deploy/gke-marketplace-app/trt-engine/README.md index 01475e551c..dba63cf63e 100644 --- a/deploy/gke-marketplace-app/trt-engine/README.md +++ b/deploy/gke-marketplace-app/trt-engine/README.md @@ -33,7 +33,7 @@ ``` docker run --gpus all -it --network host \ --shm-size=1g --ulimit memlock=-1 --ulimit stack=67108864 \ - -v ~:/scripts nvcr.io/nvidia/tensorrt:25.05-py3 + -v ~:/scripts nvcr.io/nvidia/tensorrt:25.06-py3 pip install onnx six torch tf2onnx tensorflow @@ -57,7 +57,7 @@ mkdir -p engines python3 builder.py -m models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/model.ckpt -o engines/bert_large_int8_bs1_s128.engine -b 1 -s 128 -c models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/ -v models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/vocab.txt --int8 --fp16 --strict --calib-num 1 -iln -imh -gsutil cp bert_large_int8_bs1_s128.engine gs://triton_sample_models/25.05/bert/1/model.plan +gsutil cp bert_large_int8_bs1_s128.engine gs://triton_sample_models/25.06/bert/1/model.plan ``` -For each Triton upgrade, container version used to generate the model, and the model path in GCS `gs://triton_sample_models/25.05/` should be updated accordingly with the correct version. +For each Triton upgrade, container version used to generate the model, and the model path in GCS `gs://triton_sample_models/25.06/` should be updated accordingly with the correct version. diff --git a/deploy/k8s-onprem/values.yaml b/deploy/k8s-onprem/values.yaml index 27cb9bf33b..ac6d7a3480 100644 --- a/deploy/k8s-onprem/values.yaml +++ b/deploy/k8s-onprem/values.yaml @@ -30,7 +30,7 @@ tags: openshift: false image: - imageName: nvcr.io/nvidia/tritonserver:25.05-py3 + imageName: nvcr.io/nvidia/tritonserver:25.06-py3 pullPolicy: IfNotPresent modelRepositoryServer: < Replace with the IP Address of your file server > modelRepositoryPath: /srv/models diff --git a/deploy/oci/values.yaml b/deploy/oci/values.yaml index b082864f3e..be6eb13a0e 100644 --- a/deploy/oci/values.yaml +++ b/deploy/oci/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.05-py3 + imageName: nvcr.io/nvidia/tritonserver:25.06-py3 pullPolicy: IfNotPresent modelRepositoryPath: s3://https://.compat.objectstorage..oraclecloud.com:443/triton-inference-server-repository numGpus: 1 diff --git a/docs/introduction/compatibility.md b/docs/introduction/compatibility.md index afa4ab0ac8..4332d4c8ae 100644 --- a/docs/introduction/compatibility.md +++ b/docs/introduction/compatibility.md @@ -38,20 +38,21 @@ | Triton release version | NGC Tag | Python version | Torch version | TensorRT version | TensorRT-LLM version | CUDA version | CUDA Driver version | Size | | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 25.06 | nvcr.io/nvidia/tritonserver:25.06-trtllm-python-py3 | Python 3.12.3 | 2.7.0a0+79aa17489c.nv25.4 | 10.10.0.31 | 0.20.0 | 12.9.0.036 | 575.51.03 | 18.3G | | 25.05 | nvcr.io/nvidia/tritonserver:25.05-trtllm-python-py3 | Python 3.12.3 | 2.7.0a0+7c8ec84dab.nv25.3 | 10.9.0.34 | 0.19.0 | 12.8.1.012 | 570.124.06 | 17G | | 25.04 | nvcr.io/nvidia/tritonserver:25.04-trtllm-python-py3 | Python 3.12.3 | 2.7.0a0+7c8ec84dab.nv25.3 | 10.9.0.34 | 0.18.2 | 12.8.1.012 | 570.124.06 | 17G | -| 25.03 | nvcr.io/nvidia/tritonserver:25.03-trtllm-python-py3 | Python 3.12.3 | 2.7.0a0%2B7c8ec84dab.nv25.3 | 10.9.0.34 | 0.18.0 | 12.8.1.012 | 570.124.06 | 28G | -| 25.02 | nvcr.io/nvidia/tritonserver:25.02-trtllm-python-py3 | Python 3.12.3 | 2.6.0a0%2Becf3bae40a.nv25.1 | 10.8.0.43 | 0.17.0.post1 | 12.8.0.038 | 570.86.10 | 28G | -| 25.01 | nvcr.io/nvidia/tritonserver:25.01-trtllm-python-py3 | Python 3.12.3 | 2.6.0a0%2Becf3bae40a.nv25.1 | 10.8.0.43 | 0.17.0 | 12.8.0.038 | 570.86.10 | 30G | -| 24.12 | nvcr.io/nvidia/tritonserver:24.12-trtllm-python-py3 | Python 3.12.3 | 2.6.0a0%2Bdf5bbc09d1.nv24.11 | 10.7.0 | 0.16.0 | 12.6.3 | 560.35.05 | 22G | -| 24.11 | nvcr.io/nvidia/tritonserver:24.11-trtllm-python-py3 | Python 3.10.12 | 2.5.0a0%2Be000cf0ad9.nv24.10 | 10.6.0 | 0.15.0 | 12.6.3 | 555.42.06 | 24.8G | -| 24.10 | nvcr.io/nvidia/tritonserver:24.10-trtllm-python-py3 | Python 3.10.12 | 2.4.0a0%2B3bcc3cddb5.nv24.7 | 10.4.0 | 0.14.0 | 12.5.1.007 | 555.42.06 | 23.3G | -| 24.09 | nvcr.io/nvidia/tritonserver:24.09-trtllm-python-py3 | Python 3.10.12 | 2.4.0a0%2B3bcc3cddb5.nv24.7 | 10.4.0 | 0.13.0 | 12.5.1.007 | 555.42.06 | 21G | -| 24.08 | nvcr.io/nvidia/tritonserver:24.08-trtllm-python-py3 | Python 3.10.12 | 2.4.0a0%2B3bcc3cddb5.nv24.7 | 10.3.0 | 0.12.0 | 12.5.1.007 | 555.42.06 | 21G | -| 24.07 | nvcr.io/nvidia/tritonserver:24.07-trtllm-python-py3 | Python 3.10.12 | 2.4.0a0%2B07cecf4168.nv24.5 | 10.1.0 | 0.11.0 | 12.4.1.003 | 550.54.15 | 23G | -| 24.06 | nvcr.io/nvidia/tritonserver:24.06-trtllm-python-py3 | Python 3.10.12 | 2.3.0a0%2B40ec155e58.nv24.3 | 10.0.1 | 0.10.0 | 12.4.0.041 | 550.54.14 | 31G | -| 24.05 | nvcr.io/nvidia/tritonserver:24.05-trtllm-python-py3 | Python 3.10.12 | 2.3.0a0%2Bebedce2 | 10.0.1.6 | 0.9.0 | 12.3.2.001 | 545.23.08 | 34G | -| 24.04 | nvcr.io/nvidia/tritonserver:24.04-trtllm-python-py3 | Python 3.10.12 | 2.3.0a0%2Bebedce2 | 9.3.0.post12.dev1 | 0.9.0 | 12.3.2.001 | 545.23.08 | 34G | +| 25.03 | nvcr.io/nvidia/tritonserver:25.03-trtllm-python-py3 | Python 3.12.3 | 2.7.0a0+7c8ec84dab.nv25.3 | 10.9.0.34 | 0.18.0 | 12.8.1.012 | 570.124.06 | 28G | +| 25.02 | nvcr.io/nvidia/tritonserver:25.02-trtllm-python-py3 | Python 3.12.3 | 2.6.0a0+ecf3bae40a.nv25.1 | 10.8.0.43 | 0.17.0.post1 | 12.8.0.038 | 570.86.10 | 28G | +| 25.01 | nvcr.io/nvidia/tritonserver:25.01-trtllm-python-py3 | Python 3.12.3 | 2.6.0a0+ecf3bae40a.nv25.1 | 10.8.0.43 | 0.17.0 | 12.8.0.038 | 570.86.10 | 30G | +| 24.12 | nvcr.io/nvidia/tritonserver:24.12-trtllm-python-py3 | Python 3.12.3 | 2.6.0a0+df5bbc09d1.nv24.11 | 10.7.0 | 0.16.0 | 12.6.3 | 560.35.05 | 22G | +| 24.11 | nvcr.io/nvidia/tritonserver:24.11-trtllm-python-py3 | Python 3.10.12 | 2.5.0a0+e000cf0ad9.nv24.10 | 10.6.0 | 0.15.0 | 12.6.3 | 555.42.06 | 24.8G | +| 24.10 | nvcr.io/nvidia/tritonserver:24.10-trtllm-python-py3 | Python 3.10.12 | 2.4.0a0+3bcc3cddb5.nv24.7 | 10.4.0 | 0.14.0 | 12.5.1.007 | 555.42.06 | 23.3G | +| 24.09 | nvcr.io/nvidia/tritonserver:24.09-trtllm-python-py3 | Python 3.10.12 | 2.4.0a0+3bcc3cddb5.nv24.7 | 10.4.0 | 0.13.0 | 12.5.1.007 | 555.42.06 | 21G | +| 24.08 | nvcr.io/nvidia/tritonserver:24.08-trtllm-python-py3 | Python 3.10.12 | 2.4.0a0+3bcc3cddb5.nv24.7 | 10.3.0 | 0.12.0 | 12.5.1.007 | 555.42.06 | 21G | +| 24.07 | nvcr.io/nvidia/tritonserver:24.07-trtllm-python-py3 | Python 3.10.12 | 2.4.0a0+07cecf4168.nv24.5 | 10.1.0 | 0.11.0 | 12.4.1.003 | 550.54.15 | 23G | +| 24.06 | nvcr.io/nvidia/tritonserver:24.06-trtllm-python-py3 | Python 3.10.12 | 2.3.0a0+40ec155e58.nv24.3 | 10.0.1 | 0.10.0 | 12.4.0.041 | 550.54.14 | 31G | +| 24.05 | nvcr.io/nvidia/tritonserver:24.05-trtllm-python-py3 | Python 3.10.12 | 2.3.0a0+ebedce2 | 10.0.1.6 | 0.9.0 | 12.3.2.001 | 545.23.08 | 34G | +| 24.04 | nvcr.io/nvidia/tritonserver:24.04-trtllm-python-py3 | Python 3.10.12 | 2.3.0a0+ebedce2 | 9.3.0.post12.dev1 | 0.9.0 | 12.3.2.001 | 545.23.08 | 34G | ## Container Name: vllm-python-py3 diff --git a/docs/introduction/release_notes.md b/docs/introduction/release_notes.md index f297ec3ac2..7038abbb01 100644 --- a/docs/introduction/release_notes.md +++ b/docs/introduction/release_notes.md @@ -25,9 +25,9 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --> -# [Triton Inference Server Release 25.05](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-25-05.html#rel-25-05) +# [Triton Inference Server Release 25.06](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-25-06.html#rel-25-06) -The Triton Inference Server container image, release 25.05, is available +The Triton Inference Server container image, release 25.06, is available on [NGC](https://ngc.nvidia.com/catalog/containers/nvidia:tritonserver) and is open source on [GitHub](https://github.com/triton-inference-server/server). Release notes can diff --git a/python/openai/README.md b/python/openai/README.md index 3d770caa2e..e93c9c1f16 100644 --- a/python/openai/README.md +++ b/python/openai/README.md @@ -51,7 +51,7 @@ docker run -it --net=host --gpus all --rm \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ -e HF_TOKEN \ - nvcr.io/nvidia/tritonserver:25.05-vllm-python-py3 + nvcr.io/nvidia/tritonserver:25.06-vllm-python-py3 ``` 2. Launch the OpenAI-compatible Triton Inference Server: diff --git a/qa/L0_client_nobatch/test.sh b/qa/L0_client_nobatch/test.sh index cb1a05d660..0c01bafb70 100755 --- a/qa/L0_client_nobatch/test.sh +++ b/qa/L0_client_nobatch/test.sh @@ -47,7 +47,7 @@ EXPECTED_NUM_TESTS="4" DATADIR=/data/inferenceserver/${REPO_VERSION} MODELDIR="${PWD}/qa_model_repository" -rm -rf ${MODELDIR} && cp -r "${DATADIR}/qa_model_repository/onnx_*" ${MODELDIR} # Note there is a coupling in ./client_test.py +rm -rf ${MODELDIR} && mkdir -p ${MODELDIR} && cp -r ${DATADIR}/qa_model_repository/onnx_* ${MODELDIR}/. # Note there is a coupling in ./client_test.py SERVER=/opt/tritonserver/bin/tritonserver SERVER_ARGS="--model-repository=${MODELDIR}" SERVER_LOG="./inference_server.log" diff --git a/qa/L0_long_running_stress/scenarios.py b/qa/L0_long_running_stress/scenarios.py index 650f228ab5..5b952f5b97 100755 --- a/qa/L0_long_running_stress/scenarios.py +++ b/qa/L0_long_running_stress/scenarios.py @@ -203,7 +203,7 @@ def __init__( # Add no validation models self.options_.append( PerfAnalyzerScenario.ModelOption( - "resnet_v1_50_def", 32, (1, 4, 1), queue_latency_range_us + "resnet_v1_50", 32, (1, 4, 1), queue_latency_range_us ) ) for trial in sequence_trials: @@ -334,7 +334,7 @@ def run(self, client_metadata): class ResNetScenario(Scenario): def __init__(self, name, batch_size=32, verbose=False, out_stream=sys.stdout): super().__init__(name, [], verbose, out_stream) - self.model_name_ = "resnet_v1_50_def" + self.model_name_ = "resnet_v1_50" self.batch_size_ = batch_size img = self.preprocess("../images/vulture.jpeg") @@ -353,7 +353,7 @@ def preprocess(self, filename): return scaled def postprocess(self, results): - output_array = results.as_numpy("resnet_v1_50/predictions/Softmax") + output_array = results.as_numpy("resnet_v1_50/predictions/Softmax:0") if len(output_array) != self.batch_size_: raise Exception( "expected {} results, got {}".format( @@ -377,12 +377,12 @@ def postprocess(self, results): def run(self, client_metadata): triton_client = client_metadata[0] - inputs = [grpcclient.InferInput("input", self.image_data_.shape, "FP32")] + inputs = [grpcclient.InferInput("input:0", self.image_data_.shape, "FP32")] inputs[0].set_data_from_numpy(self.image_data_) outputs = [ grpcclient.InferRequestedOutput( - "resnet_v1_50/predictions/Softmax", class_count=1 + "resnet_v1_50/predictions/Softmax:0", class_count=1 ) ] res = triton_client.infer(self.model_name_, inputs, outputs=outputs) diff --git a/qa/L0_long_running_stress/test.sh b/qa/L0_long_running_stress/test.sh index 1973028bcf..92d8c09b0d 100755 --- a/qa/L0_long_running_stress/test.sh +++ b/qa/L0_long_running_stress/test.sh @@ -130,9 +130,8 @@ cp -r ../custom_models/custom_zero_1_float32 $MODEL_DIR/custom_zero_1_float32 && echo "{ key: \"execute_delay_ms\"; value: { string_value: \"10000\" }}" >> config.pbtxt && \ echo "]" >> config.pbtxt) -cp -r $DATADIR/onnx_model_store/resnet_v1_50 $MODEL_DIR/resnet_v1_50_def && \ - (cd $MODEL_DIR/resnet_v1_50_def && \ - sed -i 's/^name: "resnet_v1_50"/name: "resnet_v1_50_def"/' config.pbtxt && \ +cp -r $DATADIR/onnx_model_store/resnet_v1_50 $MODEL_DIR/. && \ + (cd $MODEL_DIR/resnet_v1_50 && \ echo "optimization { }" >> config.pbtxt) SERVER_ARGS="--model-repository=`pwd`/$MODEL_DIR" diff --git a/qa/common/gen_jetson_trt_models b/qa/common/gen_jetson_trt_models index 8726e7c271..b081fe9c4e 100755 --- a/qa/common/gen_jetson_trt_models +++ b/qa/common/gen_jetson_trt_models @@ -34,7 +34,7 @@ # Make all generated files accessible outside of container umask 0000 # Set the version of the models -TRITON_VERSION=${TRITON_VERSION:=25.05} +TRITON_VERSION=${TRITON_VERSION:=25.06} # Set the CUDA device to use CUDA_DEVICE=${RUNNER_ID:=0} # Set TensorRT image diff --git a/qa/common/gen_qa_custom_ops b/qa/common/gen_qa_custom_ops index 96801e4470..108b1daa53 100755 --- a/qa/common/gen_qa_custom_ops +++ b/qa/common/gen_qa_custom_ops @@ -37,7 +37,7 @@ ## ############################################################################ -TRITON_VERSION=${TRITON_VERSION:=25.05} +TRITON_VERSION=${TRITON_VERSION:=25.06} NVIDIA_UPSTREAM_VERSION=${NVIDIA_UPSTREAM_VERSION:=$TRITON_VERSION} PYTORCH_IMAGE=${PYTORCH_IMAGE:=nvcr.io/nvidia/pytorch:$NVIDIA_UPSTREAM_VERSION-py3} UBUNTU_IMAGE=${UBUNTU_IMAGE:=ubuntu:24.04} diff --git a/qa/common/gen_qa_model_repository b/qa/common/gen_qa_model_repository index 7f4741d1c7..2deeb97da5 100755 --- a/qa/common/gen_qa_model_repository +++ b/qa/common/gen_qa_model_repository @@ -48,7 +48,7 @@ ## ############################################################################ -TRITON_VERSION=${TRITON_VERSION:=25.05} +TRITON_VERSION=${TRITON_VERSION:=25.06} # ONNX. Use ONNX_OPSET 0 to use the default for ONNX version ONNX_VERSION=1.16.1 From cc476d7e879410c1c7fa56fdf2008186d44a5c27 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Mon, 30 Jun 2025 05:56:53 -0700 Subject: [PATCH 049/230] test: Remove shared memory key from the error response (#8269) --- qa/python_models/bls_memory/model.py | 4 ++-- qa/python_models/bls_memory_async/model.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qa/python_models/bls_memory/model.py b/qa/python_models/bls_memory/model.py index 69da4f440f..a55da15774 100644 --- a/qa/python_models/bls_memory/model.py +++ b/qa/python_models/bls_memory/model.py @@ -1,4 +1,4 @@ -# Copyright 2021-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -60,7 +60,7 @@ def test_bls_out_of_memory(self): input0_np, infer_response = self._send_identity_tensor( tensor_size, self._is_decoupled ) - out_of_memory_message = "Failed to increase the shared memory pool size for key" + out_of_memory_message = "Failed to increase the shared memory pool size" if infer_response.has_error(): self.assertIn(out_of_memory_message, infer_response.error().message()) diff --git a/qa/python_models/bls_memory_async/model.py b/qa/python_models/bls_memory_async/model.py index d9e676b42e..c96073eac0 100644 --- a/qa/python_models/bls_memory_async/model.py +++ b/qa/python_models/bls_memory_async/model.py @@ -1,4 +1,4 @@ -# Copyright 2021-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -53,7 +53,7 @@ async def test_bls_out_of_memory(): tensor_size = 256 * 1024 * 1024 input0_np, infer_response = await _send_identity_tensor(tensor_size, is_decoupled) - out_of_memory_message = "Failed to increase the shared memory pool size for key" + out_of_memory_message = "Failed to increase the shared memory pool size" if infer_response.has_error(): if not (out_of_memory_message in infer_response.error().message()): From 251f8ae4b2a566ae2c0b25df727eb6f42ab4795c Mon Sep 17 00:00:00 2001 From: Sai Kiran Polisetty Date: Tue, 1 Jul 2025 12:25:02 +0530 Subject: [PATCH 050/230] fix: Improve data type validation for classification (#8267) Co-authored-by: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> --- qa/L0_parameters/class_count_test.py | 105 +++++++++++++++++++++++++++ qa/L0_parameters/test.sh | 59 ++++++++++++++- src/classification.cc | 16 +++- 3 files changed, 175 insertions(+), 5 deletions(-) create mode 100755 qa/L0_parameters/class_count_test.py diff --git a/qa/L0_parameters/class_count_test.py b/qa/L0_parameters/class_count_test.py new file mode 100755 index 0000000000..9e49ae4ae2 --- /dev/null +++ b/qa/L0_parameters/class_count_test.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 + +# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import sys + +sys.path.append("../common") + +import os +import unittest + +import numpy as np +import test_util as tu +import tritonclient.grpc as grpcclient +import tritonclient.http as httpclient +from tritonclient.utils import InferenceServerException + + +class ClassificationParameterTest(tu.TestResultCollector): + def setUp(self): + self.protocol = os.environ.get("CLIENT_TYPE", "http") + if self.protocol == "http": + self.client = httpclient.InferenceServerClient("localhost:8000") + else: + self.client = grpcclient.InferenceServerClient("localhost:8001") + + def _prepare_io(self, input_data, dtype): + if self.protocol == "http": + inputs = [httpclient.InferInput("INPUT0", input_data.shape, dtype)] + outputs = [httpclient.InferRequestedOutput(name="OUTPUT0", class_count=5)] + else: + inputs = [grpcclient.InferInput("INPUT0", input_data.shape, dtype)] + outputs = [grpcclient.InferRequestedOutput(name="OUTPUT0", class_count=5)] + inputs[0].set_data_from_numpy(input_data) + return inputs, outputs + + def test_classificattion(self): + shape = (1, 8) + dtype = "FP32" + model_name = "identity_fp32" + input_data = np.ones(shape, dtype=np.float32) + + inputs, outputs = self._prepare_io(input_data, dtype) + result = self.client.infer( + model_name=model_name, inputs=inputs, outputs=outputs + ) + output = result.get_output("OUTPUT0") + if self.protocol == "http": + output_dtype = output["datatype"] + else: + output_dtype = output.datatype + + self.assertEqual(output_dtype, "BYTES") + + # Validate shape matches to the class_count + output_data = result.as_numpy("OUTPUT0") + self.assertIsNotNone(output_data) + self.assertEqual(output_data.shape, (1, 5)) + + for res_str_bytes in np.nditer(output_data, flags=["refs_ok"]): + res_str = res_str_bytes.item().decode("utf-8") + self.assertTrue(res_str.startswith("1.000000:")) + + def test_classificattion_unsupported_data_type(self): + shape = (1, 8) + model_name = "identity_bytes" + dtype = "BYTES" + input_data = np.array([["test"] * shape[1]], dtype=object) + + inputs, outputs = self._prepare_io(input_data, dtype) + with self.assertRaises(InferenceServerException) as e: + self.client.infer(model_name=model_name, inputs=inputs, outputs=outputs) + + self.assertIn( + "class result not available for output due to unsupported type 'BYTES'", + str(e.exception), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/qa/L0_parameters/test.sh b/qa/L0_parameters/test.sh index c53b02d4b7..63868237aa 100755 --- a/qa/L0_parameters/test.sh +++ b/qa/L0_parameters/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023-2024, NVIDIA CORPORATION. All rights reserved. +# Copyright 2023-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -97,10 +97,65 @@ for i in "${all_tests[@]}"; do wait $SERVER_PID done + +# Test Classification Extension +PYTHON_MODELS_DIR="${PYTHON_MODELS_DIR:-/opt/tritonserver/qa/python_models}" +MODELDIR="models" +TEST_RESULT_FILE="test_results.txt" +TEST_SCRIPT_PY="./class_count_test.py" + +rm -rf $MODELDIR +mkdir -p "${MODELDIR}/identity_fp32/1" +cp ${PYTHON_MODELS_DIR}/identity_fp32/config.pbtxt "${MODELDIR}/identity_fp32/" +cp ${PYTHON_MODELS_DIR}/identity_fp32/model.py "${MODELDIR}/identity_fp32/1/" + +mkdir -p "${MODELDIR}/identity_bytes/1" +cp ${PYTHON_MODELS_DIR}/identity_fp32/config.pbtxt "${MODELDIR}/identity_bytes/" +cp ${PYTHON_MODELS_DIR}/identity_fp32/model.py "${MODELDIR}/identity_bytes/1/" +(cd "${MODELDIR}/identity_bytes" && \ + sed -i 's/identity_fp32/identity_bytes/' config.pbtxt && \ + sed -i 's/TYPE_FP32/TYPE_STRING/' config.pbtxt ) + +SERVER_ARGS="--model-repository=`pwd`/${MODELDIR} --log-verbose=1" +for client_type in http grpc; do + export CLIENT_TYPE=$client_type + SERVER_LOG="./class_count_test_${client_type}_server.log" + CLIENT_LOG="./class_count_test_${client_type}_client.log" + rm -f $SERVER_LOG $CLIENT_LOG + run_server + if [ "$SERVER_PID" == "0" ]; then + echo -e "\n***\n*** Failed to start $SERVER\n***" + cat $SERVER_LOG + exit 1 + fi + + set +e + python3 $TEST_SCRIPT_PY -v >>"$CLIENT_LOG" 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed - class_count_${client_type}_test_client\n***" + RET=1 + else + check_test_results $TEST_RESULT_FILE 2 + if [ $? -ne 0 ]; then + cat $TEST_RESULT_FILE + echo -e "\n***\n*** Test Result Verification Failed - class_count_${client_type}_test_client\n***" + RET=1 + fi + fi + kill $SERVER_PID + wait $SERVER_PID + + if [ $? -ne 0 ]; then + echo -e "\n***\n*** Test Server shut down non-gracefully\n***" + RET=1 + fi + set -e +done + if [ $RET -eq 0 ]; then echo -e "\n***\n*** Test Passed\n***" else - cat $CLIENT_LOG echo -e "\n***\n*** Test FAILED\n***" fi diff --git a/src/classification.cc b/src/classification.cc index 2d8cd26b9e..828edf613d 100644 --- a/src/classification.cc +++ b/src/classification.cc @@ -1,4 +1,4 @@ -// Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. +// Copyright (c) 2020-2025, NVIDIA CORPORATION. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -77,8 +77,18 @@ TopkClassifications( const TRITONSERVER_DataType datatype, const uint32_t req_class_count, std::vector* class_strs) { - const size_t element_cnt = - byte_size / TRITONSERVER_DataTypeByteSize(datatype); + const uint32_t dtype_byte_size = TRITONSERVER_DataTypeByteSize(datatype); + if (dtype_byte_size == 0) { + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + std::string( + std::string("class result not available for output due to " + "unsupported type '") + + std::string(TRITONSERVER_DataTypeString(datatype)) + "'") + .c_str()); + } + + const size_t element_cnt = byte_size / dtype_byte_size; switch (datatype) { case TRITONSERVER_TYPE_UINT8: From c104b890794d0455e2e0e573525e332e5d8bf622 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Tue, 1 Jul 2025 12:23:34 -0700 Subject: [PATCH 051/230] TPRD-1590: OpenVINO 2025.2.0 version updated (#8277) --- build.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.py b/build.py index ff069c7d20..48f7f56c73 100755 --- a/build.py +++ b/build.py @@ -75,8 +75,8 @@ "triton_container_version": "25.07dev", "upstream_container_version": "25.06", "ort_version": "1.22.0", - "ort_openvino_version": "2025.1.0", - "standalone_openvino_version": "2025.1.0", + "ort_openvino_version": "2025.2.0", + "standalone_openvino_version": "2025.2.0", "dcgm_version": "3.3.6", "vllm_version": "0.9.0.1", "rhel_py_version": "3.12.3", From 2e8de237fb362ed5900773408193079732094002 Mon Sep 17 00:00:00 2001 From: Sai Kiran Polisetty Date: Wed, 2 Jul 2025 17:49:01 +0530 Subject: [PATCH 052/230] fix: Improve validation for system shared memory register (#8273) --- qa/L0_shared_memory/shared_memory_test.py | 18 ++++++++++++++++++ qa/L0_shared_memory/test.sh | 1 + src/common.h | 4 ++++ src/shared_memory_manager.cc | 12 +++++++++++- 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/qa/L0_shared_memory/shared_memory_test.py b/qa/L0_shared_memory/shared_memory_test.py index 146025cf26..35667aacfa 100755 --- a/qa/L0_shared_memory/shared_memory_test.py +++ b/qa/L0_shared_memory/shared_memory_test.py @@ -509,6 +509,24 @@ def test_python_client_leak(self): "client memory usage is increasing", ) + def test_register_reserved_names(self): + """ + Test that registration fails if attempting to use a reserved + prefix for the shm key. + """ + # This matches kTritonSharedMemoryRegionPrefix in the server code. + reserved_prefix = "triton_python_backend_shm_region_" + + # The shared memory key cannot start with the reserved prefix. + shm_name = "my_test_shm_name" + shm_key = f"{reserved_prefix}_my_test_shm_key" + + with self.assertRaisesRegex( + utils.InferenceServerException, + f"cannot register shared memory region '{shm_name}' with key '{shm_key}' as the key contains the reserved prefix '{reserved_prefix}'", + ) as e: + self.triton_client.register_system_shared_memory(shm_name, shm_key, 10000) + def callback(user_data, result, error): if error: diff --git a/qa/L0_shared_memory/test.sh b/qa/L0_shared_memory/test.sh index 97b81397ec..55cdb80951 100755 --- a/qa/L0_shared_memory/test.sh +++ b/qa/L0_shared_memory/test.sh @@ -55,6 +55,7 @@ for i in \ test_infer_byte_size_out_of_bound \ test_infer_integer_overflow \ test_register_out_of_bound \ + test_register_reserved_names \ test_python_client_leak; do for client_type in http grpc; do SERVER_ARGS="--model-repository=`pwd`/models --log-verbose=1 ${SERVER_ARGS_EXTRA}" diff --git a/src/common.h b/src/common.h index fe7e3be4cd..7df618e965 100644 --- a/src/common.h +++ b/src/common.h @@ -45,6 +45,10 @@ constexpr char kContentEncodingHTTPHeader[] = "Content-Encoding"; constexpr char kContentTypeHeader[] = "Content-Type"; constexpr char kContentLengthHeader[] = "Content-Length"; +// This prefix is reserved for shm regions created internally by Triton +constexpr char kTritonSharedMemoryRegionPrefix[] = + "triton_python_backend_shm_region_"; + constexpr int MAX_GRPC_MESSAGE_SIZE = INT32_MAX; /// The value for a dimension in a shape that indicates that that diff --git a/src/shared_memory_manager.cc b/src/shared_memory_manager.cc index 17b0e43da0..80b739ee1b 100644 --- a/src/shared_memory_manager.cc +++ b/src/shared_memory_manager.cc @@ -354,6 +354,17 @@ SharedMemoryManager::RegisterSystemSharedMemory( const std::string& name, const std::string& shm_key, const size_t offset, const size_t byte_size) { + // Check if the shared memory key starts with the reserved prefix + if (shm_key.rfind(kTritonSharedMemoryRegionPrefix, 0) == 0) { + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + std::string( + "cannot register shared memory region '" + name + "' with key '" + + shm_key + "' as the key contains the reserved prefix '" + + kTritonSharedMemoryRegionPrefix + "'") + .c_str()); + } + std::lock_guard lock(mu_); if (shared_memory_map_.find(name) != shared_memory_map_.end()) { @@ -670,7 +681,6 @@ SharedMemoryManager::UnregisterAll(TRITONSERVER_MemoryType memory_type) ++next_it; if (it->second->kind_ == TRITONSERVER_MEMORY_GPU) { TRITONSERVER_Error* err = UnregisterHelper(it->first, memory_type); - ; if (err != nullptr) { unregister_fails.push_back(it->first); LOG_VERBOSE(1) << TRITONSERVER_ErrorMessage(err); From d17512bcd787428b002becd60c6da48c72c90c2e Mon Sep 17 00:00:00 2001 From: Sai Kiran Polisetty Date: Thu, 3 Jul 2025 11:17:28 +0530 Subject: [PATCH 053/230] feat: Add support for `usage` in the OpenAI frontend vLLM backend (#8264) --- python/openai/README.md | 22 +- .../openai_frontend/engine/triton_engine.py | 90 +++++- .../openai_frontend/engine/utils/triton.py | 84 ++++- .../openai/openai_frontend/schemas/openai.py | 16 + python/openai/tests/test_chat_completions.py | 39 ++- python/openai/tests/test_completions.py | 35 +- python/openai/tests/test_lora.py | 12 + python/openai/tests/test_openai_client.py | 303 +++++++++++++++++- 8 files changed, 573 insertions(+), 28 deletions(-) diff --git a/python/openai/README.md b/python/openai/README.md index e93c9c1f16..7d77620a55 100644 --- a/python/openai/README.md +++ b/python/openai/README.md @@ -98,7 +98,7 @@ curl -s http://localhost:9000/v1/chat/completions -H 'Content-Type: application/ ```json { - "id": "cmpl-6930b296-7ef8-11ef-bdd1-107c6149ca79", + "id": "cmpl-0242093d-51ae-11f0-b339-e7480668bfbe", "choices": [ { "finish_reason": "stop", @@ -113,11 +113,15 @@ curl -s http://localhost:9000/v1/chat/completions -H 'Content-Type: application/ "logprobs": null } ], - "created": 1727679085, + "created": 1750846825, "model": "llama-3.1-8b-instruct", "system_fingerprint": null, "object": "chat.completion", - "usage": null + "usage": { + "completion_tokens": 7, + "prompt_tokens": 42, + "total_tokens": 49 + } } ``` @@ -138,20 +142,24 @@ curl -s http://localhost:9000/v1/completions -H 'Content-Type: application/json' ```json { - "id": "cmpl-d51df75c-7ef8-11ef-bdd1-107c6149ca79", + "id": "cmpl-58fba3a0-51ae-11f0-859d-e7480668bfbe", "choices": [ { "finish_reason": "stop", "index": 0, "logprobs": null, - "text": " a field of computer science that focuses on developing algorithms that allow computers to learn from" + "text": " an amazing field that can truly understand the hidden patterns that exist in the data," } ], - "created": 1727679266, + "created": 1750846970, "model": "llama-3.1-8b-instruct", "system_fingerprint": null, "object": "text_completion", - "usage": null + "usage": { + "completion_tokens": 16, + "prompt_tokens": 4, + "total_tokens": 20 + } } ``` diff --git a/python/openai/openai_frontend/engine/triton_engine.py b/python/openai/openai_frontend/engine/triton_engine.py index c29faa288a..499cc623e7 100644 --- a/python/openai/openai_frontend/engine/triton_engine.py +++ b/python/openai/openai_frontend/engine/triton_engine.py @@ -51,7 +51,9 @@ _create_trtllm_inference_request, _create_vllm_inference_request, _get_output, + _get_usage_from_response, _get_vllm_lora_names, + _StreamingUsageAccumulator, _validate_triton_responses_non_streaming, ) from schemas.openai import ( @@ -65,6 +67,7 @@ ChatCompletionStreamResponseDelta, ChatCompletionToolChoiceOption1, Choice, + CompletionUsage, CreateChatCompletionRequest, CreateChatCompletionResponse, CreateChatCompletionStreamResponse, @@ -229,6 +232,8 @@ async def chat( backend=metadata.backend, ) + usage = _get_usage_from_response(response, metadata.backend) + return CreateChatCompletionResponse( id=request_id, choices=[ @@ -243,6 +248,7 @@ async def chat( model=request.model, system_fingerprint=None, object=ObjectType.chat_completion, + usage=usage, ) def _get_chat_completion_response_message( @@ -319,7 +325,7 @@ async def completion( created = int(time.time()) if request.stream: return self._streaming_completion_iterator( - request_id, created, request.model, responses + request_id, created, request, responses, metadata.backend ) # Response validation with decoupled models in mind @@ -328,6 +334,8 @@ async def completion( response = responses[0] text = _get_output(response) + usage = _get_usage_from_response(response, metadata.backend) + choice = Choice( finish_reason=FinishReason.stop, index=0, @@ -341,6 +349,7 @@ async def completion( object=ObjectType.text_completion, created=created, model=request.model, + usage=usage, ) # TODO: This behavior should be tested further @@ -421,6 +430,7 @@ def _get_streaming_chat_response_chunk( request_id: str, created: int, model: str, + usage: Optional[CompletionUsage] = None, ) -> CreateChatCompletionStreamResponse: return CreateChatCompletionStreamResponse( id=request_id, @@ -429,6 +439,7 @@ def _get_streaming_chat_response_chunk( model=model, system_fingerprint=None, object=ObjectType.chat_completion_chunk, + usage=usage, ) def _get_first_streaming_chat_response( @@ -444,7 +455,7 @@ def _get_first_streaming_chat_response( finish_reason=None, ) chunk = self._get_streaming_chat_response_chunk( - choice, request_id, created, model + choice, request_id, created, model, usage=None ) return chunk @@ -470,6 +481,8 @@ async def _streaming_chat_iterator( ) previous_text = "" + include_usage = request.stream_options and request.stream_options.include_usage + usage_accumulator = _StreamingUsageAccumulator(backend) chunk = self._get_first_streaming_chat_response( request_id, created, model, role @@ -478,6 +491,8 @@ async def _streaming_chat_iterator( async for response in responses: delta_text = _get_output(response) + if include_usage: + usage_accumulator.update(response) ( response_delta, @@ -512,10 +527,25 @@ async def _streaming_chat_iterator( ) chunk = self._get_streaming_chat_response_chunk( - choice, request_id, created, model + choice, request_id, created, model, usage=None ) yield f"data: {chunk.model_dump_json(exclude_unset=True)}\n\n" + # Send the final usage chunk if requested via stream_options. + if include_usage: + usage_payload = usage_accumulator.get_final_usage() + if usage_payload: + final_usage_chunk = CreateChatCompletionStreamResponse( + id=request_id, + choices=[], + created=created, + model=model, + system_fingerprint=None, + object=ObjectType.chat_completion_chunk, + usage=usage_payload, + ) + yield f"data: {final_usage_chunk.model_dump_json(exclude_unset=True)}\n\n" + yield "data: [DONE]\n\n" def _get_streaming_response_delta( @@ -662,6 +692,18 @@ def _validate_chat_request( self._verify_chat_tool_call_settings(request=request) + if request.stream_options and not request.stream: + raise Exception("`stream_options` can only be used when `stream` is True") + + if ( + request.stream_options + and request.stream_options.include_usage + and metadata.backend != "vllm" + ): + raise Exception( + "`stream_options.include_usage` is currently only supported for the vLLM backend" + ) + def _verify_chat_tool_call_settings(self, request: CreateChatCompletionRequest): if ( request.tool_choice @@ -698,9 +740,21 @@ def _verify_chat_tool_call_settings(self, request: CreateChatCompletionRequest): ) async def _streaming_completion_iterator( - self, request_id: str, created: int, model: str, responses: AsyncIterable + self, + request_id: str, + created: int, + request: CreateCompletionRequest, + responses: AsyncIterable, + backend: str, ) -> AsyncIterator[str]: + model = request.model + include_usage = request.stream_options and request.stream_options.include_usage + usage_accumulator = _StreamingUsageAccumulator(backend) + async for response in responses: + if include_usage: + usage_accumulator.update(response) + text = _get_output(response) choice = Choice( finish_reason=FinishReason.stop if response.final else None, @@ -715,10 +769,26 @@ async def _streaming_completion_iterator( object=ObjectType.text_completion, created=created, model=model, + usage=None, ) yield f"data: {chunk.model_dump_json(exclude_unset=True)}\n\n" + # Send the final usage chunk if requested via stream_options. + if include_usage: + usage_payload = usage_accumulator.get_final_usage() + if usage_payload: + final_usage_chunk = CreateCompletionResponse( + id=request_id, + choices=[], + system_fingerprint=None, + object=ObjectType.text_completion, + created=created, + model=model, + usage=usage_payload, + ) + yield f"data: {final_usage_chunk.model_dump_json(exclude_unset=True)}\n\n" + yield "data: [DONE]\n\n" def _validate_completion_request( @@ -771,6 +841,18 @@ def _validate_completion_request( if request.logit_bias is not None or request.logprobs is not None: raise Exception("logit bias and log probs not supported") + if request.stream_options and not request.stream: + raise Exception("`stream_options` can only be used when `stream` is True") + + if ( + request.stream_options + and request.stream_options.include_usage + and metadata.backend != "vllm" + ): + raise Exception( + "`stream_options.include_usage` is currently only supported for the vLLM backend" + ) + def _should_stream_with_auto_tool_parsing( self, request: CreateChatCompletionRequest ): diff --git a/python/openai/openai_frontend/engine/utils/triton.py b/python/openai/openai_frontend/engine/utils/triton.py index 4e94a7b203..3104c49911 100644 --- a/python/openai/openai_frontend/engine/utils/triton.py +++ b/python/openai/openai_frontend/engine/utils/triton.py @@ -27,7 +27,7 @@ import json import os import re -from dataclasses import asdict +from dataclasses import asdict, dataclass, field from typing import Iterable, List, Optional, Union import numpy as np @@ -36,6 +36,7 @@ from schemas.openai import ( ChatCompletionNamedToolChoice, ChatCompletionToolChoiceOption1, + CompletionUsage, CreateChatCompletionRequest, CreateCompletionRequest, ) @@ -121,6 +122,8 @@ def _create_vllm_inference_request( # Pass sampling_parameters as serialized JSON string input to support List # fields like 'stop' that aren't supported by TRITONSERVER_Parameters yet. inputs["sampling_parameters"] = [sampling_parameters] + inputs["return_num_input_tokens"] = np.bool_([True]) + inputs["return_num_output_tokens"] = np.bool_([True]) return model.create_request(inputs=inputs) @@ -221,6 +224,85 @@ def _to_string(tensor: tritonserver.Tensor) -> str: return _construct_string_from_pointer(tensor.data_ptr + 4, tensor.size - 4) +@dataclass +class _StreamingUsageAccumulator: + """Helper class to accumulate token usage from a streaming response.""" + + backend: str + prompt_tokens: int = 0 + completion_tokens: int = 0 + _prompt_tokens_set: bool = field(init=False, default=False) + + def update(self, response: tritonserver.InferenceResponse): + """Extracts usage from a response and updates the token counts.""" + usage = _get_usage_from_response(response, self.backend) + if usage: + # The prompt_tokens is received with every chunk but should only be set once. + if not self._prompt_tokens_set: + self.prompt_tokens = usage.prompt_tokens + self._prompt_tokens_set = True + self.completion_tokens += usage.completion_tokens + + def get_final_usage(self) -> Optional[CompletionUsage]: + """ + Returns the final populated CompletionUsage object if any tokens were tracked. + """ + # If _prompt_tokens_set is True, it means we have received and processed + # at least one valid usage payload. + if self._prompt_tokens_set: + return CompletionUsage( + prompt_tokens=self.prompt_tokens, + completion_tokens=self.completion_tokens, + total_tokens=self.prompt_tokens + self.completion_tokens, + ) + return None + + +def _get_usage_from_response( + response: tritonserver._api._response.InferenceResponse, + backend: str, +) -> Optional[CompletionUsage]: + """ + Extracts token usage statistics from a Triton inference response. + """ + # TODO: Remove this check once TRT-LLM backend supports both "num_input_tokens" + # and "num_output_tokens", and also update the test cases accordingly. + if backend != "vllm": + return None + + prompt_tokens = None + completion_tokens = None + + if ( + "num_input_tokens" in response.outputs + and "num_output_tokens" in response.outputs + ): + input_token_tensor = response.outputs["num_input_tokens"] + output_token_tensor = response.outputs["num_output_tokens"] + + if input_token_tensor.data_type == tritonserver.DataType.UINT32: + prompt_tokens_ptr = ctypes.cast( + input_token_tensor.data_ptr, ctypes.POINTER(ctypes.c_uint32) + ) + prompt_tokens = prompt_tokens_ptr[0] + + if output_token_tensor.data_type == tritonserver.DataType.UINT32: + completion_tokens_ptr = ctypes.cast( + output_token_tensor.data_ptr, ctypes.POINTER(ctypes.c_uint32) + ) + completion_tokens = completion_tokens_ptr[0] + + if prompt_tokens is not None and completion_tokens is not None: + total_tokens = prompt_tokens + completion_tokens + return CompletionUsage( + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + total_tokens=total_tokens, + ) + + return None + + # TODO: Use tritonserver.InferenceResponse when support is published def _get_output(response: tritonserver._api._response.InferenceResponse) -> str: if "text_output" in response.outputs: diff --git a/python/openai/openai_frontend/schemas/openai.py b/python/openai/openai_frontend/schemas/openai.py index 86d75cb09d..f13296163d 100644 --- a/python/openai/openai_frontend/schemas/openai.py +++ b/python/openai/openai_frontend/schemas/openai.py @@ -133,6 +133,10 @@ class CreateCompletionRequest(BaseModel): False, description="Whether to stream back partial progress. If set, tokens will be sent as data-only [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) as they become available, with the stream terminated by a `data: [DONE]` message. [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions).\n", ) + stream_options: Optional[StreamOptions] = Field( + None, + description="Options for streaming responses. Only use when `stream` is set to `true`.", + ) suffix: Optional[str] = Field( None, description="The suffix that comes after a completion of inserted text.\n\nThis parameter is only supported for `gpt-3.5-turbo-instruct`.\n", @@ -467,6 +471,13 @@ class ResponseFormat(BaseModel): ) +class StreamOptions(BaseModel): + include_usage: Optional[bool] = Field( + False, + description="If enabled, an additional chunk is sent before the `data: [DONE]` message. That chunk’s `usage` field reports the total token usage for the request and its `choices` array is always empty. All other chunks include a `usage` field with a null value.", + ) + + class FunctionCall3(Enum): none = "none" auto = "auto" @@ -565,6 +576,7 @@ class CreateChatCompletionStreamResponse(BaseModel): object: Object4 = Field( ..., description="The object type, which is always `chat.completion.chunk`." ) + usage: Optional[CompletionUsage] = None class CreateChatCompletionImageResponse(BaseModel): @@ -881,6 +893,10 @@ class CreateChatCompletionRequest(BaseModel): False, description="If set, partial message deltas will be sent, like in ChatGPT. Tokens will be sent as data-only [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) as they become available, with the stream terminated by a `data: [DONE]` message. [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions).\n", ) + stream_options: Optional[StreamOptions] = Field( + None, + description="Options for streaming responses. Only use when `stream` is set to `true`.", + ) temperature: Optional[confloat(ge=0.0, le=2.0)] = Field( 0.7, description="What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\n\nWe generally recommend altering this or `top_p` but not both.\n", diff --git a/python/openai/tests/test_chat_completions.py b/python/openai/tests/test_chat_completions.py index ccead85e70..5402be451d 100644 --- a/python/openai/tests/test_chat_completions.py +++ b/python/openai/tests/test_chat_completions.py @@ -41,7 +41,9 @@ class TestChatCompletions: def client(self, fastapi_client_class_scope): yield fastapi_client_class_scope - def test_chat_completions_defaults(self, client, model: str, messages: List[dict]): + def test_chat_completions_defaults( + self, client, model: str, messages: List[dict], backend: str + ): response = client.post( "/v1/chat/completions", json={"model": model, "messages": messages}, @@ -51,8 +53,12 @@ def test_chat_completions_defaults(self, client, model: str, messages: List[dict message = response.json()["choices"][0]["message"] assert message["content"].strip() assert message["role"] == "assistant" - # "usage" currently not supported - assert not response.json()["usage"] + + usage = response.json().get("usage") + if backend == "vllm": + assert usage is not None + else: + assert usage is None def test_chat_completions_system_prompt(self, client, model: str): # NOTE: Currently just sanity check that there are no issues when a @@ -530,9 +536,30 @@ def test_request_logprobs(self): def test_request_logit_bias(self): pass - @pytest.mark.skip(reason="Not Implemented Yet") - def test_usage_response(self): - pass + def test_usage_response( + self, client, model: str, messages: List[dict], backend: str + ): + if backend != "vllm": + pytest.skip( + "Usage reporting is currently available only for the vLLM backend." + ) + + response = client.post( + "/v1/chat/completions", + json={"model": model, "messages": messages}, + ) + + assert response.status_code == 200 + usage = response.json().get("usage") + assert usage is not None + assert isinstance(usage["prompt_tokens"], int) + assert isinstance(usage["completion_tokens"], int) + assert isinstance(usage["total_tokens"], int) + assert usage["prompt_tokens"] > 0 + assert usage["completion_tokens"] > 0 + assert ( + usage["total_tokens"] == usage["prompt_tokens"] + usage["completion_tokens"] + ) # For tests that won't use the same pytest fixture for server startup across diff --git a/python/openai/tests/test_completions.py b/python/openai/tests/test_completions.py index 1a411a0124..9ec3ffe7f7 100644 --- a/python/openai/tests/test_completions.py +++ b/python/openai/tests/test_completions.py @@ -35,7 +35,7 @@ class TestCompletions: def client(self, fastapi_client_class_scope): yield fastapi_client_class_scope - def test_completions_defaults(self, client, model: str, prompt: str): + def test_completions_defaults(self, client, model: str, prompt: str, backend: str): response = client.post( "/v1/completions", json={"model": model, "prompt": prompt}, @@ -46,8 +46,12 @@ def test_completions_defaults(self, client, model: str, prompt: str): # NOTE: Could be improved to look for certain quality of response, # or tested with dummy identity model. assert response.json()["choices"][0]["text"].strip() - # "usage" currently not supported - assert not response.json()["usage"] + + usage = response.json().get("usage") + if backend == "vllm": + assert usage is not None + else: + assert usage is None @pytest.mark.parametrize( "sampling_parameter, value", @@ -367,6 +371,25 @@ def test_lora(self): def test_multi_lora(self): pass - @pytest.mark.skip(reason="Not Implemented Yet") - def test_usage_response(self): - pass + def test_usage_response(self, client, model: str, prompt: str, backend: str): + if backend != "vllm": + pytest.skip( + "Usage reporting is currently available only for the vLLM backend." + ) + + response = client.post( + "/v1/completions", + json={"model": model, "prompt": prompt}, + ) + + assert response.status_code == 200 + usage = response.json().get("usage") + assert usage is not None + assert isinstance(usage["prompt_tokens"], int) + assert isinstance(usage["completion_tokens"], int) + assert isinstance(usage["total_tokens"], int) + assert usage["prompt_tokens"] > 0 + assert usage["completion_tokens"] > 0 + assert ( + usage["total_tokens"] == usage["prompt_tokens"] + usage["completion_tokens"] + ) diff --git a/python/openai/tests/test_lora.py b/python/openai/tests/test_lora.py index 2c89789bbc..4a5ff10361 100644 --- a/python/openai/tests/test_lora.py +++ b/python/openai/tests/test_lora.py @@ -131,6 +131,18 @@ def _create_model_repository_mock_llm(self): name: "exclude_input_in_output" data_type: TYPE_BOOL dims: [ 1 ] + }, + { + name: "return_num_input_tokens" + data_type: TYPE_BOOL + dims: [1] + optional: true + }, + { + name: "return_num_output_tokens" + data_type: TYPE_BOOL + dims: [1] + optional: true } ] output [ diff --git a/python/openai/tests/test_openai_client.py b/python/openai/tests/test_openai_client.py index efcc31f93e..1a1001329b 100644 --- a/python/openai/tests/test_openai_client.py +++ b/python/openai/tests/test_openai_client.py @@ -49,7 +49,7 @@ def test_openai_client_models(self, client: openai.OpenAI, backend: str): raise Exception(f"Unexpected backend {backend=}") def test_openai_client_completion( - self, client: openai.OpenAI, model: str, prompt: str + self, client: openai.OpenAI, model: str, prompt: str, backend: str ): completion = client.completions.create( prompt=prompt, @@ -60,8 +60,20 @@ def test_openai_client_completion( assert completion.choices[0].text assert completion.choices[0].finish_reason == "stop" + usage = completion.usage + if backend == "vllm": + assert usage is not None + assert isinstance(usage.prompt_tokens, int) + assert isinstance(usage.completion_tokens, int) + assert isinstance(usage.total_tokens, int) + assert usage.prompt_tokens > 0 + assert usage.completion_tokens > 0 + assert usage.total_tokens == usage.prompt_tokens + usage.completion_tokens + else: + assert usage is None + def test_openai_client_chat_completion( - self, client: openai.OpenAI, model: str, messages: List[dict] + self, client: openai.OpenAI, model: str, messages: List[dict], backend: str ): chat_completion = client.chat.completions.create( messages=messages, @@ -72,6 +84,18 @@ def test_openai_client_chat_completion( assert chat_completion.choices[0].message.content assert chat_completion.choices[0].finish_reason == "stop" + usage = chat_completion.usage + if backend == "vllm": + assert usage is not None + assert isinstance(usage.prompt_tokens, int) + assert isinstance(usage.completion_tokens, int) + assert isinstance(usage.total_tokens, int) + assert usage.prompt_tokens > 0 + assert usage.completion_tokens > 0 + assert usage.total_tokens == usage.prompt_tokens + usage.completion_tokens + else: + assert usage is None + @pytest.mark.parametrize("echo", [False, True]) def test_openai_client_completion_echo( self, client: openai.OpenAI, echo: bool, backend: str, model: str, prompt: str @@ -117,7 +141,7 @@ async def test_openai_client_models(self, client: openai.AsyncOpenAI, backend: s @pytest.mark.asyncio async def test_openai_client_completion( - self, client: openai.AsyncOpenAI, model: str, prompt: str + self, client: openai.AsyncOpenAI, model: str, prompt: str, backend: str ): completion = await client.completions.create( prompt=prompt, @@ -128,9 +152,21 @@ async def test_openai_client_completion( assert completion.choices[0].text assert completion.choices[0].finish_reason == "stop" + usage = completion.usage + if backend == "vllm": + assert usage is not None + assert isinstance(usage.prompt_tokens, int) + assert isinstance(usage.completion_tokens, int) + assert isinstance(usage.total_tokens, int) + assert usage.prompt_tokens > 0 + assert usage.completion_tokens > 0 + assert usage.total_tokens == usage.prompt_tokens + usage.completion_tokens + else: + assert usage is None + @pytest.mark.asyncio async def test_openai_client_chat_completion( - self, client: openai.AsyncOpenAI, model: str, messages: List[dict] + self, client: openai.AsyncOpenAI, model: str, messages: List[dict], backend: str ): chat_completion = await client.chat.completions.create( messages=messages, @@ -139,6 +175,19 @@ async def test_openai_client_chat_completion( assert chat_completion.choices[0].message.content assert chat_completion.choices[0].finish_reason == "stop" + + usage = chat_completion.usage + if backend == "vllm": + assert usage is not None + assert isinstance(usage.prompt_tokens, int) + assert isinstance(usage.completion_tokens, int) + assert isinstance(usage.total_tokens, int) + assert usage.prompt_tokens > 0 + assert usage.completion_tokens > 0 + assert usage.total_tokens == usage.prompt_tokens + usage.completion_tokens + else: + assert usage is None + print(f"Chat completion results: {chat_completion}") @pytest.mark.asyncio @@ -237,6 +286,7 @@ async def test_chat_streaming( chunks.append(delta.content) if chunk.choices[0].finish_reason is not None: finish_reason_count += 1 + assert chunk.usage is None # finish reason should only return in last block assert finish_reason_count == 1 @@ -247,3 +297,248 @@ async def test_chat_streaming( assert len(chunks) > 1 streamed_output = "".join(chunks) assert streamed_output == output + + @pytest.mark.asyncio + async def test_chat_streaming_usage_option( + self, client: openai.AsyncOpenAI, model: str, messages: List[dict], backend: str + ): + if backend != "vllm": + pytest.skip( + "Usage reporting is currently available only for the vLLM backend." + ) + + seed = 0 + temperature = 0.0 + max_tokens = 16 + + # Get usage and content from a non-streaming call + stream_false = await client.chat.completions.create( + model=model, + messages=messages, + max_tokens=max_tokens, + temperature=temperature, + seed=seed, + stream=False, + ) + usage_stream_false = stream_false.usage + stream_false_output = stream_false.choices[0].message.content + assert usage_stream_false is not None + assert stream_false_output is not None + + # First, run with include_usage=False. + stream_options_false = await client.chat.completions.create( + model=model, + messages=messages, + max_tokens=max_tokens, + temperature=temperature, + seed=seed, + stream=True, + stream_options={"include_usage": False}, + ) + chunks_false = [chunk async for chunk in stream_options_false] + for chunk in chunks_false: + assert chunk.usage is None, "Usage should be null when include_usage=False" + stream_options_false_output = "".join( + c.choices[0].delta.content + for c in chunks_false + if c.choices and c.choices[0].delta.content + ) + + # Now, run with include_usage=True. + stream_options_true = await client.chat.completions.create( + model=model, + messages=messages, + max_tokens=max_tokens, + temperature=temperature, + seed=seed, + stream=True, + stream_options={"include_usage": True}, + ) + chunks_true = [chunk async for chunk in stream_options_true] + content_chunks = [c for c in chunks_true if c.usage is None] + + # Verify that we received exactly one extra chunk. + assert len(chunks_true) == len(chunks_false) + 1 + + # Verify content is consistent + stream_options_true_output = "".join( + c.choices[0].delta.content + for c in content_chunks + if c.choices and c.choices[0].delta.content + ) + assert stream_options_true_output == stream_false_output + assert stream_options_true_output == stream_options_false_output + + # Verify the final chunk has usage data and empty choices. + final_chunk = chunks_true[-1] + assert final_chunk.usage is not None + assert len(final_chunk.choices) == 0 + usage_stream_options_true = final_chunk.usage + assert ( + isinstance(usage_stream_options_true.prompt_tokens, int) + and usage_stream_options_true.prompt_tokens > 0 + ) + assert ( + isinstance(usage_stream_options_true.completion_tokens, int) + and usage_stream_options_true.completion_tokens > 0 + ) + assert ( + usage_stream_options_true.total_tokens + == usage_stream_options_true.prompt_tokens + + usage_stream_options_true.completion_tokens + ) + + # Verify other chunks have no usage data. + for chunk in chunks_true[:-1]: + assert chunk.usage is None + + # Assert usage is consistent between streaming and non-streaming calls + assert usage_stream_false.model_dump() == usage_stream_options_true.model_dump() + + @pytest.mark.asyncio + async def test_completion_streaming_usage_option( + self, client: openai.AsyncOpenAI, model: str, prompt: str, backend: str + ): + if backend != "vllm": + pytest.skip( + "Usage reporting is currently available only for the vLLM backend." + ) + + seed = 0 + temperature = 0.0 + max_tokens = 16 + + # Get usage and content from a non-streaming call + stream_false = await client.completions.create( + model=model, + prompt=prompt, + max_tokens=max_tokens, + temperature=temperature, + stream=False, + seed=seed, + ) + usage_stream_false = stream_false.usage + stream_false_output = stream_false.choices[0].text + assert usage_stream_false is not None + assert stream_false_output is not None + + # First, run with include_usage=False. + stream_options_false = await client.completions.create( + model=model, + prompt=prompt, + max_tokens=max_tokens, + temperature=temperature, + seed=seed, + stream=True, + stream_options={"include_usage": False}, + ) + chunks_false = [chunk async for chunk in stream_options_false] + for chunk in chunks_false: + assert chunk.usage is None + stream_options_false_output = "".join( + c.choices[0].text for c in chunks_false if c.choices and c.choices[0].text + ) + + # Now, run with include_usage=True. + stream_options_true = await client.completions.create( + model=model, + prompt=prompt, + max_tokens=max_tokens, + temperature=temperature, + stream=True, + seed=seed, + stream_options={"include_usage": True}, + ) + chunks_true = [chunk async for chunk in stream_options_true] + content_chunks = [c for c in chunks_true if c.usage is None] + + # Verify that we received exactly one extra chunk. + assert len(chunks_true) == len(chunks_false) + 1 + + # Verify content is consistent + stream_options_true_output = "".join( + c.choices[0].text for c in content_chunks if c.choices and c.choices[0].text + ) + assert stream_options_true_output == stream_false_output + assert stream_options_true_output == stream_options_false_output + + # Verify the final chunk has usage data and empty choices. + final_chunk = chunks_true[-1] + assert final_chunk.usage is not None + assert len(final_chunk.choices) == 0 + usage_stream_options_true = final_chunk.usage + assert ( + isinstance(usage_stream_options_true.prompt_tokens, int) + and usage_stream_options_true.prompt_tokens > 0 + ) + assert ( + isinstance(usage_stream_options_true.completion_tokens, int) + and usage_stream_options_true.completion_tokens > 0 + ) + assert ( + usage_stream_options_true.total_tokens + == usage_stream_options_true.prompt_tokens + + usage_stream_options_true.completion_tokens + ) + + # Verify other chunks have no usage data. + for chunk in chunks_true[:-1]: + assert chunk.usage is None + + # Assert usage is consistent between streaming and non-streaming calls + assert usage_stream_false.model_dump() == usage_stream_options_true.model_dump() + + @pytest.mark.asyncio + async def test_stream_options_without_streaming( + self, client: openai.AsyncOpenAI, model: str, prompt: str + ): + with pytest.raises(openai.BadRequestError) as e: + await client.completions.create( + model=model, + prompt=prompt, + stream=False, + stream_options={"include_usage": True}, + ) + assert "`stream_options` can only be used when `stream` is True" in str(e.value) + + with pytest.raises(openai.BadRequestError) as e: + await client.chat.completions.create( + model=model, + messages=[{"role": "user", "content": prompt}], + stream=False, + stream_options={"include_usage": True}, + ) + assert "`stream_options` can only be used when `stream` is True" in str(e.value) + + @pytest.mark.asyncio + async def test_streaming_usage_unsupported_backend( + self, client: openai.AsyncOpenAI, model: str, messages: List[dict], backend: str + ): + if backend == "vllm": + pytest.skip( + "This test is for backends that do not support usage reporting." + ) + + with pytest.raises(openai.BadRequestError) as e: + await client.completions.create( + model=model, + prompt="Test prompt", + stream=True, + stream_options={"include_usage": True}, + ) + assert ( + "`stream_options.include_usage` is currently only supported for the vLLM backend" + in str(e.value) + ) + + with pytest.raises(openai.BadRequestError) as e: + await client.chat.completions.create( + model=model, + messages=messages, + stream=True, + stream_options={"include_usage": True}, + ) + assert ( + "`stream_options.include_usage` is currently only supported for the vLLM backend" + in str(e.value) + ) From 9d622365e37c9807075455daa287eaa7f99fe466 Mon Sep 17 00:00:00 2001 From: Kyle McGill <101670481+nv-kmcgill53@users.noreply.github.com> Date: Wed, 9 Jul 2025 18:13:31 -0700 Subject: [PATCH 054/230] fix: adding libcufile.so.0 to the cpu only container so that libtorch models can load (#8265) Co-authored-by: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> --- build.py | 1 + 1 file changed, 1 insertion(+) diff --git a/build.py b/build.py index 48f7f56c73..226f69907a 100755 --- a/build.py +++ b/build.py @@ -1573,6 +1573,7 @@ def add_cpu_libs_to_linux_dockerfile(backends, target_machine): COPY --from=min_container /usr/local/cuda/lib64/libcudart.so.12 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. COPY --from=min_container /usr/local/cuda/lib64/libcupti.so.12 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. COPY --from=min_container /usr/local/cuda/lib64/libnvJitLink.so.12 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. +COPY --from=min_container /usr/local/cuda/lib64/libcufile.so.0 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. RUN mkdir -p /opt/hpcx/ucc/lib/ /opt/hpcx/ucx/lib/ COPY --from=min_container /opt/hpcx/ucc/lib/libucc.so.1 /opt/hpcx/ucc/lib/libucc.so.1 From e870036f4844e235a93196317c435087f1fecc73 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Tue, 15 Jul 2025 11:21:39 -0700 Subject: [PATCH 055/230] DLIS-8335: Update API for model generation (#8294) --- qa/common/gen_qa_model_repository | 2 +- qa/common/test_util.py | 35 ++++++++++++++++++++++++------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/qa/common/gen_qa_model_repository b/qa/common/gen_qa_model_repository index 2deeb97da5..d7f51c8f02 100755 --- a/qa/common/gen_qa_model_repository +++ b/qa/common/gen_qa_model_repository @@ -480,4 +480,4 @@ if [ -z $CI ] ; then docker rm -f $(docker ps -a --filter volume=$DOCKER_VOLUME --format '{{ .ID }}') echo -e "\033[34m[ INFO ] - Removing Docker volume $DOCKER_VOLUME \033[0m " docker volume rm $DOCKER_VOLUME -fi \ No newline at end of file +fi diff --git a/qa/common/test_util.py b/qa/common/test_util.py index f3bbcdb16f..044de0c6f9 100755 --- a/qa/common/test_util.py +++ b/qa/common/test_util.py @@ -309,17 +309,36 @@ def check_gpus_compute_capability(min_capability): Returns: bool """ - import pycuda.driver as cuda - cuda.init() + import importlib.util - for device_index in range(cuda.Device.count()): - device = cuda.Device(device_index) - compute_capability = device.compute_capability() - compute_capability_value = compute_capability[0] + compute_capability[1] / 10.0 + if importlib.util.find_spec("cuda.core.experimental"): + import cuda.core.experimental as cuda_core_experimental - if compute_capability_value < min_capability: - return False + devices = cuda_core_experimental.system.devices + + for device in devices: + compute_capability = ( + device.compute_capability.major + device.compute_capability.minor / 10.0 + ) + + if compute_capability < min_capability: + return False + + elif importlib.util.find_spec("pycuda.driver"): + import pycuda.driver as cuda + + cuda.init() + + for device_index in range(cuda.Device.count()): + device = cuda.Device(device_index) + compute_capability = device.compute_capability() + compute_capability_value = ( + compute_capability[0] + compute_capability[1] / 10.0 + ) + + if compute_capability_value < min_capability: + return False return True From 9529d5afbbca7765ef1d9fbad2be2dc7826e697d Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Tue, 15 Jul 2025 15:23:54 -0700 Subject: [PATCH 056/230] build: TPRD-1622 - Update version to CMake 4.0 or 3.31 ( 3.31.8 ) which has support for CUDA new layout (#8285) --- CMakeLists.txt | 4 +- Dockerfile.QA | 9 +---- Dockerfile.sdk | 15 +------ Dockerfile.win10.min | 4 +- build.py | 40 ++++++------------- qa/L0_backend_bls/test.sh | 4 +- qa/L0_backend_python/common.sh | 2 +- qa/L0_backend_tutorial/test.sh | 4 +- qa/L0_batch_custom/test.sh | 2 +- qa/L0_client_build_variants/test.sh | 2 +- src/CMakeLists.txt | 2 +- src/python/CMakeLists.txt | 4 +- src/python/tritonfrontend/CMakeLists.txt | 2 +- src/test/CMakeLists.txt | 2 +- src/test/distributed_addsub/CMakeLists.txt | 4 +- src/test/dyna_sequence/CMakeLists.txt | 4 +- src/test/implicit_state/CMakeLists.txt | 4 +- src/test/iterative_sequence/CMakeLists.txt | 4 +- src/test/query_backend/CMakeLists.txt | 4 +- .../relocation_repoagent/CMakeLists.txt | 4 +- src/test/sequence/CMakeLists.txt | 4 +- 21 files changed, 47 insertions(+), 77 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7d03d85665..68f68e1a69 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2020-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -24,7 +24,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -cmake_minimum_required(VERSION 3.18) +cmake_minimum_required(VERSION 3.31.8) project(tritonserver LANGUAGES C CXX) diff --git a/Dockerfile.QA b/Dockerfile.QA index d95b6cf9e2..6cab754def 100644 --- a/Dockerfile.QA +++ b/Dockerfile.QA @@ -66,13 +66,8 @@ RUN apt-get update && \ software-properties-common && \ rm -rf /var/lib/apt/lists/* -RUN apt update -q=2 \ - && apt install -y gpg wget \ - && wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null \ - && . /etc/os-release \ - && echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $UBUNTU_CODENAME main" | tee /etc/apt/sources.list.d/kitware.list >/dev/null \ - && apt-get update -q=2 \ - && apt-get install -y --no-install-recommends cmake=3.28.3* cmake-data=3.28.3* +RUN pip3 install cmake==4.0.3 +ENV CMAKE_POLICY_VERSION_MINIMUM=3.5 # Add densenet_onnx model to example repo # FIXME: This should be changed to using the fetch_models.sh script diff --git a/Dockerfile.sdk b/Dockerfile.sdk index d1c4b5a189..f4b4e37679 100644 --- a/Dockerfile.sdk +++ b/Dockerfile.sdk @@ -56,7 +56,7 @@ FROM ${BASE_IMAGE} AS sdk_build # Ensure apt-get won't prompt for selecting options ENV DEBIAN_FRONTEND=noninteractive -ENV PIP_BREAK_SYSTEM_PACKAGES=1 +ENV PIP_BREAK_SYSTEM_PACKAGES=1 CMAKE_POLICY_VERSION_MINIMUM=3.5 RUN apt-get update && \ apt-get install -y --no-install-recommends \ @@ -86,18 +86,7 @@ RUN apt-get update && \ python3-pdfkit \ openjdk-11-jdk \ maven && \ - pip3 install --upgrade "grpcio-tools<1.68" - -# Client build requires recent version of CMake (FetchContent required) -# Using CMAKE installation instruction from:: https://apt.kitware.com/ -RUN apt update -q=2 \ - && apt install -y gpg wget \ - && wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null \ - && . /etc/os-release \ - && echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $UBUNTU_CODENAME main" | tee /etc/apt/sources.list.d/kitware.list >/dev/null \ - && apt-get update -q=2 \ - && apt-get install -y --no-install-recommends cmake=3.28.3* cmake-data=3.28.3* \ - && cmake --version + pip3 install --upgrade "grpcio-tools<1.68" cmake==4.0.3 # Build expects "python" executable (not python3). RUN rm -f /usr/bin/python && \ diff --git a/Dockerfile.win10.min b/Dockerfile.win10.min index b883ad4dcb..c940256b50 100644 --- a/Dockerfile.win10.min +++ b/Dockerfile.win10.min @@ -87,9 +87,11 @@ LABEL PYTHON_VERSION=${PYTHON_VERSION} # # Installing CMake # -ARG CMAKE_VERSION=3.30.5 +ARG CMAKE_VERSION=4.0.3 RUN pip install cmake==%CMAKE_VERSION% +ENV CMAKE_POLICY_VERSION_MINIMUM=3.5 + ENV CMAKE_TOOLCHAIN_FILE /vcpkg/scripts/buildsystems/vcpkg.cmake ENV VCPKG_TARGET_TRIPLET x64-windows diff --git a/build.py b/build.py index 226f69907a..6250a74421 100755 --- a/build.py +++ b/build.py @@ -917,7 +917,7 @@ def create_dockerfile_buildbase_rhel(ddir, dockerfile_name, argmap): ARG TRITON_VERSION ARG TRITON_CONTAINER_VERSION -ENV PIP_BREAK_SYSTEM_PACKAGES=1 +ENV PIP_BREAK_SYSTEM_PACKAGES=1 CMAKE_POLICY_VERSION_MINIMUM=3.5 """ df += """ # Install docker docker buildx @@ -988,7 +988,8 @@ def create_dockerfile_buildbase_rhel(ddir, dockerfile_name, argmap): setuptools \\ docker \\ virtualenv \\ - patchelf==0.17.2 + patchelf==0.17.2 \\ + cmake==4.0.3 # Install boost version >= 1.78 for boost::span # Current libboost-dev apt packages are < 1.78, so install from tar.gz @@ -996,16 +997,6 @@ def create_dockerfile_buildbase_rhel(ddir, dockerfile_name, argmap): https://archives.boost.io/release/1.80.0/source/boost_1_80_0.tar.gz \\ && (cd /tmp && tar xzf boost.tar.gz) \\ && mv /tmp/boost_1_80_0/boost /usr/include/boost - -# Server build requires recent version of CMake (FetchContent required) -# Might not need this if the installed version of cmake is high enough for our build. -# RUN apt update -q=2 \\ -# && apt install -y gpg wget \\ -# && wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null \\ -# && . /etc/os-release \\ -# && echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $UBUNTU_CODENAME main" | tee /etc/apt/sources.list.d/kitware.list >/dev/null \\ -# && apt-get update -q=2 \\ -# && apt-get install -y --no-install-recommends cmake=3.27.7* cmake-data=3.27.7* """ if FLAGS.enable_gpu: df += install_dcgm_libraries(argmap["DCGM_VERSION"], target_machine()) @@ -1041,7 +1032,7 @@ def create_dockerfile_buildbase(ddir, dockerfile_name, argmap): ARG TRITON_VERSION ARG TRITON_CONTAINER_VERSION -ENV PIP_BREAK_SYSTEM_PACKAGES=1 +ENV PIP_BREAK_SYSTEM_PACKAGES=1 CMAKE_POLICY_VERSION_MINIMUM=3.5 """ # Install the windows- or linux-specific buildbase dependencies if target_platform() == "windows": @@ -1108,7 +1099,8 @@ def create_dockerfile_buildbase(ddir, dockerfile_name, argmap): build \\ docker \\ virtualenv \\ - patchelf==0.17.2 + patchelf==0.17.2 \\ + cmake==4.0.3 # Install boost version >= 1.78 for boost::span # Current libboost-dev apt packages are < 1.78, so install from tar.gz @@ -1117,14 +1109,6 @@ def create_dockerfile_buildbase(ddir, dockerfile_name, argmap): && (cd /tmp && tar xzf boost.tar.gz) \\ && mv /tmp/boost_1_80_0/boost /usr/include/boost -# Server build requires recent version of CMake (FetchContent required) -RUN apt update -q=2 \\ - && apt install -y gpg wget \\ - && wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null \\ - && . /etc/os-release \\ - && echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $UBUNTU_CODENAME main" | tee /etc/apt/sources.list.d/kitware.list >/dev/null \\ - && apt-get update -q=2 \\ - && apt-get install -y --no-install-recommends cmake=3.28.3* cmake-data=3.28.3* """ if FLAGS.enable_gpu: @@ -1281,8 +1265,8 @@ def create_dockerfile_linux( rm -fr ${TRT_ROOT}/doc ${TRT_ROOT}/onnx_graphsurgeon ${TRT_ROOT}/python && \\ rm -fr ${TRT_ROOT}/samples ${TRT_ROOT}/targets/${ARCH}-linux-gnu/samples && \\ pip3 install --no-cache-dir transformers && \\ - find /usr -name libtensorrt_llm.so -exec dirname {} \; > /etc/ld.so.conf.d/tensorrt-llm.conf && \\ - find /opt/tritonserver -name libtritonserver.so -exec dirname {} \; > /etc/ld.so.conf.d/triton-tensorrtllm-worker.conf && \\ + find /usr -name libtensorrt_llm.so -exec dirname {} \\; > /etc/ld.so.conf.d/tensorrt-llm.conf && \\ + find /opt/tritonserver -name libtritonserver.so -exec dirname {} \\; > /etc/ld.so.conf.d/triton-tensorrtllm-worker.conf && \\ pip3 install --no-cache-dir grpcio-tools==1.64.0 && \\ pip3 uninstall -y setuptools ENV LD_LIBRARY_PATH=/usr/local/tensorrt/lib/:/opt/tritonserver/backends/tensorrtllm:$LD_LIBRARY_PATH @@ -1649,11 +1633,11 @@ def create_dockerfile_windows( ARG TRITON_VERSION ARG TRITON_CONTAINER_VERSION -ENV TRITON_SERVER_VERSION ${{TRITON_VERSION}} -ENV NVIDIA_TRITON_SERVER_VERSION ${{TRITON_CONTAINER_VERSION}} +ENV TRITON_SERVER_VERSION=${{TRITON_VERSION}} +ENV NVIDIA_TRITON_SERVER_VERSION=${{TRITON_CONTAINER_VERSION}} LABEL com.nvidia.tritonserver.version="${{TRITON_SERVER_VERSION}}" -RUN setx path "%path%;C:\opt\tritonserver\bin" +RUN setx path "%path%;C:\\opt\\tritonserver\\bin" """.format( argmap["TRITON_VERSION"], @@ -1838,7 +1822,7 @@ def create_docker_build_script(script_name, container_install_dir, container_ci_ if target_platform() == "windows": if FLAGS.container_memory: runargs += ["--memory", FLAGS.container_memory] - runargs += ["-v", "\\\\.\pipe\docker_engine:\\\\.\pipe\docker_engine"] + runargs += ["-v", "\\\\.\\pipe\\docker_engine:\\\\.\\pipe\\docker_engine"] else: runargs += ["-v", "/var/run/docker.sock:/var/run/docker.sock"] if FLAGS.use_user_docker_config: diff --git a/qa/L0_backend_bls/test.sh b/qa/L0_backend_bls/test.sh index 6210e7654c..7dcdbd2028 100755 --- a/qa/L0_backend_bls/test.sh +++ b/qa/L0_backend_bls/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -45,7 +45,7 @@ apt update -q=2 \ && . /etc/os-release \ && echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $UBUNTU_CODENAME main" | tee /etc/apt/sources.list.d/kitware.list >/dev/null \ && apt-get update -q=2 \ - && apt-get install -y --no-install-recommends cmake=3.28.3* cmake-data=3.28.3* \ + && apt-get install -y --no-install-recommends cmake=4.0.3* cmake-data=4.0.3* \ rapidjson-dev cmake --version diff --git a/qa/L0_backend_python/common.sh b/qa/L0_backend_python/common.sh index c6872cdf4c..68bafb4420 100755 --- a/qa/L0_backend_python/common.sh +++ b/qa/L0_backend_python/common.sh @@ -51,7 +51,7 @@ install_build_deps_apt() { && . /etc/os-release \ && echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $UBUNTU_CODENAME main" | tee /etc/apt/sources.list.d/kitware.list >/dev/null \ && apt-get update -q=2 \ - && apt-get install -y --no-install-recommends cmake=3.28.3* cmake-data=3.28.3* + && apt-get install -y --no-install-recommends cmake=4.0.3* cmake-data=4.0.3* } install_build_deps_yum() { diff --git a/qa/L0_backend_tutorial/test.sh b/qa/L0_backend_tutorial/test.sh index 3e59503b8c..b58a5e2606 100755 --- a/qa/L0_backend_tutorial/test.sh +++ b/qa/L0_backend_tutorial/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -48,7 +48,7 @@ apt update -q=2 \ && . /etc/os-release \ && echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $UBUNTU_CODENAME main" | tee /etc/apt/sources.list.d/kitware.list >/dev/null \ && apt-get update -q=2 \ - && apt-get install -y --no-install-recommends cmake=3.28.3* cmake-data=3.28.3* \ + && apt-get install -y --no-install-recommends cmake=4.0.3* cmake-data=4.0.3* \ rapidjson-dev cmake --version diff --git a/qa/L0_batch_custom/test.sh b/qa/L0_batch_custom/test.sh index 5c6824897b..c438862f5f 100755 --- a/qa/L0_batch_custom/test.sh +++ b/qa/L0_batch_custom/test.sh @@ -68,7 +68,7 @@ apt update -q=2 \ && . /etc/os-release \ && echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $UBUNTU_CODENAME main" | tee /etc/apt/sources.list.d/kitware.list >/dev/null \ && apt-get update -q=2 \ - && apt-get install -y --no-install-recommends cmake=3.28.3* cmake-data=3.28.3* rapidjson-dev + && apt-get install -y --no-install-recommends cmake=4.0.3* cmake-data=4.0.3* rapidjson-dev cmake --version # Set up repository diff --git a/qa/L0_client_build_variants/test.sh b/qa/L0_client_build_variants/test.sh index 6ff7eb4916..91926e8a0f 100755 --- a/qa/L0_client_build_variants/test.sh +++ b/qa/L0_client_build_variants/test.sh @@ -38,7 +38,7 @@ apt update -q=2 \ && . /etc/os-release \ && echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $UBUNTU_CODENAME main" | tee /etc/apt/sources.list.d/kitware.list >/dev/null \ && apt-get update -q=2 \ - && apt-get install -y --no-install-recommends cmake=3.28.3* cmake-data=3.28.3* + && apt-get install -y --no-install-recommends cmake=4.0.3* cmake-data=4.0.3* cmake --version diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8824e2ed6a..9568f4280d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -24,7 +24,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -cmake_minimum_required (VERSION 3.18) +cmake_minimum_required (VERSION 3.31.8) project(tritonserverexe LANGUAGES C CXX) diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 58208b5c8c..693c5615d7 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -24,7 +24,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -cmake_minimum_required(VERSION 3.18) +cmake_minimum_required(VERSION 3.31.8) add_subdirectory(tritonfrontend) diff --git a/src/python/tritonfrontend/CMakeLists.txt b/src/python/tritonfrontend/CMakeLists.txt index eb8716676e..fd5c4232fa 100644 --- a/src/python/tritonfrontend/CMakeLists.txt +++ b/src/python/tritonfrontend/CMakeLists.txt @@ -24,7 +24,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -cmake_minimum_required(VERSION 3.18) +cmake_minimum_required(VERSION 3.31.8) # ================= Ensures Package is Structured Properly ================== # Top level module entry point and typed marker diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 23243bf9d9..a91901af78 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -24,7 +24,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -cmake_minimum_required (VERSION 3.18) +cmake_minimum_required (VERSION 3.31.8) # # Unit tests diff --git a/src/test/distributed_addsub/CMakeLists.txt b/src/test/distributed_addsub/CMakeLists.txt index 10a7148f8a..597e70308c 100644 --- a/src/test/distributed_addsub/CMakeLists.txt +++ b/src/test/distributed_addsub/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2021-2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -24,7 +24,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -cmake_minimum_required(VERSION 3.17) +cmake_minimum_required(VERSION 3.31.8) project(tritondistributedaddsubbackend LANGUAGES C CXX) diff --git a/src/test/dyna_sequence/CMakeLists.txt b/src/test/dyna_sequence/CMakeLists.txt index 1d0c095b9d..2bed7c2d36 100644 --- a/src/test/dyna_sequence/CMakeLists.txt +++ b/src/test/dyna_sequence/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2021-2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -24,7 +24,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -cmake_minimum_required(VERSION 3.17) +cmake_minimum_required(VERSION 3.31.8) project(tritondynasequencebackend LANGUAGES C CXX) diff --git a/src/test/implicit_state/CMakeLists.txt b/src/test/implicit_state/CMakeLists.txt index c868c5f1b7..4b6024a9c7 100644 --- a/src/test/implicit_state/CMakeLists.txt +++ b/src/test/implicit_state/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -24,7 +24,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -cmake_minimum_required(VERSION 3.17) +cmake_minimum_required(VERSION 3.31.8) project(tritonimplicitsequencebackend LANGUAGES C CXX) diff --git a/src/test/iterative_sequence/CMakeLists.txt b/src/test/iterative_sequence/CMakeLists.txt index 9321e32049..4a44925524 100644 --- a/src/test/iterative_sequence/CMakeLists.txt +++ b/src/test/iterative_sequence/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -24,7 +24,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -cmake_minimum_required(VERSION 3.17) +cmake_minimum_required(VERSION 3.31.8) project(tritoniterativesequencebackend LANGUAGES C CXX) diff --git a/src/test/query_backend/CMakeLists.txt b/src/test/query_backend/CMakeLists.txt index 809499cd4b..30c33a6b50 100644 --- a/src/test/query_backend/CMakeLists.txt +++ b/src/test/query_backend/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2021-2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -24,7 +24,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -cmake_minimum_required(VERSION 3.17) +cmake_minimum_required(VERSION 3.31.8) project(tritonquerybackend LANGUAGES C CXX) diff --git a/src/test/repoagent/relocation_repoagent/CMakeLists.txt b/src/test/repoagent/relocation_repoagent/CMakeLists.txt index 8c2e27be36..d0b97b5d24 100644 --- a/src/test/repoagent/relocation_repoagent/CMakeLists.txt +++ b/src/test/repoagent/relocation_repoagent/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2021-2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -24,7 +24,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -cmake_minimum_required(VERSION 3.17) +cmake_minimum_required(VERSION 3.31.8) project(tritonrelocationrepoagent LANGUAGES C CXX) diff --git a/src/test/sequence/CMakeLists.txt b/src/test/sequence/CMakeLists.txt index 5cb8b65362..ab105f20eb 100644 --- a/src/test/sequence/CMakeLists.txt +++ b/src/test/sequence/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2021-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -24,7 +24,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -cmake_minimum_required(VERSION 3.17) +cmake_minimum_required(VERSION 3.31.8) project(tritonsequencebackend LANGUAGES C CXX) From d491fff4de61be110cbba26ec562032a38347871 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Thu, 17 Jul 2025 14:44:15 -0700 Subject: [PATCH 057/230] TPRD-1627: update validation condition (#8297) --- qa/common/test_util.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/qa/common/test_util.py b/qa/common/test_util.py index 044de0c6f9..ec8108072f 100755 --- a/qa/common/test_util.py +++ b/qa/common/test_util.py @@ -312,7 +312,7 @@ def check_gpus_compute_capability(min_capability): import importlib.util - if importlib.util.find_spec("cuda.core.experimental"): + if importlib.util.find_spec("cuda") is not None: import cuda.core.experimental as cuda_core_experimental devices = cuda_core_experimental.system.devices @@ -325,7 +325,7 @@ def check_gpus_compute_capability(min_capability): if compute_capability < min_capability: return False - elif importlib.util.find_spec("pycuda.driver"): + elif importlib.util.find_spec("pycuda") is not None: import pycuda.driver as cuda cuda.init() @@ -339,6 +339,10 @@ def check_gpus_compute_capability(min_capability): if compute_capability_value < min_capability: return False + else: + raise RuntimeError( + "No packages found to determine the compute capability. Please check the environment." + ) return True From ace2c5b9fdf52237796cb3ea4f952f83d2ef8561 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Fri, 18 Jul 2025 16:10:08 -0700 Subject: [PATCH 058/230] ci: Update DCGM version to 4.2.3-2 (#8298) --- Dockerfile.sdk | 4 ++-- build.py | 14 +++++++------- compose.py | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Dockerfile.sdk b/Dockerfile.sdk index f4b4e37679..7d114e2ce5 100644 --- a/Dockerfile.sdk +++ b/Dockerfile.sdk @@ -43,7 +43,7 @@ ARG JAVA_BINDINGS_MAVEN_VERSION=3.8.4 ARG JAVA_BINDINGS_JAVACPP_PRESETS_TAG=1.5.8 ARG TRITON_PERF_ANALYZER_BUILD=1 # DCGM version to install for Model Analyzer -ARG DCGM_VERSION=3.3.6 +ARG DCGM_VERSION=4.2.3-2 ARG NVIDIA_TRITON_SERVER_SDK_VERSION=unknown ARG NVIDIA_BUILD_ID=unknown @@ -265,7 +265,7 @@ RUN if [ "$TRITON_ENABLE_GPU" = "ON" ]; then \ curl -o /tmp/cuda-keyring.deb \ https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/$arch/cuda-keyring_1.1-1_all.deb \ && apt install /tmp/cuda-keyring.deb && rm /tmp/cuda-keyring.deb && \ - apt-get update && apt-get install -y datacenter-gpu-manager=1:${DCGM_VERSION}; \ + apt update && apt install --yes datacenter-gpu-manager-4-dev=1:${DCGM_VERSION}; \ fi # Build expects "python" executable (not python3). diff --git a/build.py b/build.py index 6250a74421..230b4dc88c 100755 --- a/build.py +++ b/build.py @@ -77,7 +77,7 @@ "ort_version": "1.22.0", "ort_openvino_version": "2025.2.0", "standalone_openvino_version": "2025.2.0", - "dcgm_version": "3.3.6", + "dcgm_version": "4.2.3-2", "vllm_version": "0.9.0.1", "rhel_py_version": "3.12.3", } @@ -858,7 +858,7 @@ def install_dcgm_libraries(dcgm_version, target_machine): # Install DCGM. Steps from https://developer.nvidia.com/dcgm#Downloads RUN dnf config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel8/sbsa/cuda-rhel8.repo \\ && dnf clean expire-cache \\ - && dnf install -y datacenter-gpu-manager-{} + && dnf install --assumeyes datacenter-gpu-manager-4-devel=1:{} """.format( dcgm_version, dcgm_version ) @@ -868,7 +868,7 @@ def install_dcgm_libraries(dcgm_version, target_machine): # Install DCGM. Steps from https://developer.nvidia.com/dcgm#Downloads RUN dnf config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel8/x86_64/cuda-rhel8.repo \\ && dnf clean expire-cache \\ - && dnf install -y datacenter-gpu-manager-{} + && dnf install --assumeyes datacenter-gpu-manager-4-devel=1:{} """.format( dcgm_version, dcgm_version ) @@ -881,8 +881,8 @@ def install_dcgm_libraries(dcgm_version, target_machine): https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/sbsa/cuda-keyring_1.1-1_all.deb \\ && apt install /tmp/cuda-keyring.deb \\ && rm /tmp/cuda-keyring.deb \\ - && apt-get update \\ - && apt-get install -y datacenter-gpu-manager=1:{} + && apt update \\ + && apt install --yes datacenter-gpu-manager-4-dev=1:{} """.format( dcgm_version, dcgm_version ) @@ -894,8 +894,8 @@ def install_dcgm_libraries(dcgm_version, target_machine): https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/x86_64/cuda-keyring_1.1-1_all.deb \\ && apt install /tmp/cuda-keyring.deb \\ && rm /tmp/cuda-keyring.deb \\ - && apt-get update \\ - && apt-get install -y datacenter-gpu-manager=1:{} + && apt update \\ + && apt install --yes datacenter-gpu-manager-4-dev=1:{} """.format( dcgm_version, dcgm_version ) diff --git a/compose.py b/compose.py index 88c6b15bf3..ad177d56b2 100755 --- a/compose.py +++ b/compose.py @@ -298,7 +298,7 @@ def create_argmap(images, skip_pull): dcgm_ver = re.search("DCGM_VERSION=([\S]{4,}) ", vars) dcgm_version = "" if dcgm_ver is None: - dcgm_version = "3.3.6" + dcgm_version = "4.2.3-2" log( "WARNING: DCGM version not found from image, installing the earlierst version {}".format( dcgm_version From ad72741a8b0e0088c30c7b5bb71144b6e7211704 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:37:33 -0700 Subject: [PATCH 059/230] refactor: Enhance memory allocation security in HTTP and Sagemaker request handler (#8305) --- qa/L0_http/http_request_many_chunks.py | 153 +++++ qa/L0_http/test.sh | 29 +- .../sagemaker_request_many_chunks.py | 91 +++ qa/L0_sagemaker/test.sh | 47 ++ src/http_server.cc | 547 ++++++++---------- src/http_server.h | 30 +- src/sagemaker_server.cc | 83 +-- 7 files changed, 606 insertions(+), 374 deletions(-) create mode 100755 qa/L0_http/http_request_many_chunks.py create mode 100755 qa/L0_sagemaker/sagemaker_request_many_chunks.py diff --git a/qa/L0_http/http_request_many_chunks.py b/qa/L0_http/http_request_many_chunks.py new file mode 100755 index 0000000000..a67d9a82c5 --- /dev/null +++ b/qa/L0_http/http_request_many_chunks.py @@ -0,0 +1,153 @@ +#!/usr/bin/python +# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import socket +import unittest + + +class HTTPRequestManyChunksTest(unittest.TestCase): + def setUp(self): + self._model_name = "simple" + self._local_host = "localhost" + self._http_port = 8000 + self._malicious_chunk_count = ( + 1000000 # large enough to cause a stack overflow if using alloca() + ) + self._parse_error = ( + "failed to parse the request JSON buffer: Invalid value. at 0" + ) + + def send_chunked_request( + self, header: str, chunk_count: int, expected_response: str + ): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + header = ( + f"{header}" + f"Host: {self._local_host}:{self._http_port}\r\n" + f"Content-Type: application/octet-stream\r\n" + f"Transfer-Encoding: chunked\r\n" + f"Connection: close\r\n" + f"\r\n" + ) + try: + s.connect((self._local_host, self._http_port)) + # HTTP request with chunked encoding + s.sendall((header.encode())) + + # Send chunked payload + for _ in range(chunk_count): + s.send(b"1\r\nA\r\n") + # End chunked encoding + s.sendall(b"0\r\n\r\n") + + # Receive response + response = b"" + while True: + try: + chunk = s.recv(4096) + if not chunk: + break + response += chunk + except socket.timeout: + break + self.assertIn(expected_response, response.decode()) + except Exception as e: + raise (e) + finally: + s.close() + + def test_infer(self): + request_header = ( + f"POST /v2/models/{self._model_name}/infer HTTP/1.1\r\n" + f"Inference-Header-Content-Length: 0\r\n" + ) + + self.send_chunked_request( + request_header, + self._malicious_chunk_count, + "Raw request must only have 1 input (found 1) to be deduced but got 2 inputs in 'simple' model configuration", + ) + + def test_registry_index(self): + request_header = f"POST /v2/repository/index HTTP/1.1\r\n" + + self.send_chunked_request( + request_header, self._malicious_chunk_count, self._parse_error + ) + + def test_model_control(self): + load_request_header = ( + f"POST /v2/repository/models/{self._model_name}/load HTTP/1.1\r\n" + ) + unload_request_header = load_request_header.replace("/load", "/unload") + + self.send_chunked_request( + load_request_header, self._malicious_chunk_count, self._parse_error + ) + self.send_chunked_request( + unload_request_header, self._malicious_chunk_count, self._parse_error + ) + + def test_trace(self): + request_header = ( + f"POST /v2/models/{self._model_name}/trace/setting HTTP/1.1\r\n" + ) + + self.send_chunked_request( + request_header, self._malicious_chunk_count, self._parse_error + ) + + def test_logging(self): + request_header = f"POST /v2/logging HTTP/1.1\r\n" + + self.send_chunked_request( + request_header, self._malicious_chunk_count, self._parse_error + ) + + def test_system_shm_register(self): + request_header = f"POST /v2/systemsharedmemory/region/test_system_shm_register/register HTTP/1.1\r\n" + + self.send_chunked_request( + request_header, self._malicious_chunk_count, self._parse_error + ) + + def test_cuda_shm_register(self): + request_header = f"POST /v2/cudasharedmemory/region/test_cuda_shm_register/register HTTP/1.1\r\n" + + self.send_chunked_request( + request_header, self._malicious_chunk_count, self._parse_error + ) + + def test_generate(self): + request_header = f"POST /v2/models/{self._model_name}/generate HTTP/1.1\r\n" + self.send_chunked_request( + request_header, self._malicious_chunk_count, self._parse_error + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/qa/L0_http/test.sh b/qa/L0_http/test.sh index a96bd6a1c6..0fc25501a3 100755 --- a/qa/L0_http/test.sh +++ b/qa/L0_http/test.sh @@ -40,6 +40,7 @@ fi export CUDA_VISIBLE_DEVICES=0 +source ../common/util.sh RET=0 CLIENT_PLUGIN_TEST="./http_client_plugin_test.py" @@ -129,7 +130,6 @@ set -e CLIENT_LOG=`pwd`/client.log SERVER_ARGS="--backend-directory=${BACKEND_DIR} --model-repository=${MODELDIR}" -source ../common/util.sh run_server if [ "$SERVER_PID" == "0" ]; then @@ -855,7 +855,32 @@ elif [ `grep -c "Error: --http-max-input-size must be greater than 0." ${SERVER_ RET=1 fi -### +### Test HTTP Requests Containing Many Chunks ### +MODELDIR="`pwd`/models" +REQUEST_MANY_CHUNKS_PY="http_request_many_chunks.py" +CLIENT_LOG="./client.http_request_many_chunks.log" +SERVER_ARGS="--model-repository=${MODELDIR} --log-verbose=1 --model-control-mode=explicit --load-model=simple" +SERVER_LOG="./inference_server_request_many_chunks.log" + +run_server +if [ "$SERVER_PID" == "0" ]; then + echo -e "\n***\n*** Failed to start $SERVER\n***" + cat $SERVER_LOG + exit 1 +fi + +set +e +python $REQUEST_MANY_CHUNKS_PY -v >> ${CLIENT_LOG} 2>&1 +if [ $? -ne 0 ]; then + echo -e "\n***\n*** HTTP Request Many Chunks Test Failed\n***" + cat $SERVER_LOG + cat $CLIENT_LOG + RET=1 +fi +set -e + +kill $SERVER_PID +wait $SERVER_PID if [ $RET -eq 0 ]; then echo -e "\n***\n*** Test Passed\n***" diff --git a/qa/L0_sagemaker/sagemaker_request_many_chunks.py b/qa/L0_sagemaker/sagemaker_request_many_chunks.py new file mode 100755 index 0000000000..415ddcba28 --- /dev/null +++ b/qa/L0_sagemaker/sagemaker_request_many_chunks.py @@ -0,0 +1,91 @@ +#!/usr/bin/python +# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import socket +import unittest + + +class SagemakerRequestManyChunksTest(unittest.TestCase): + def setUp(self): + self._local_host = "localhost" + self._sagemaker_port = 8080 + self._malicious_chunk_count = ( + 1000000 # large enough to cause a stack overflow if using alloca() + ) + + def send_chunked_request( + self, header: str, chunk_count: int, expected_response: str + ): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + header = ( + f"{header}" + f"Host: {self._local_host}:{self._sagemaker_port}\r\n" + f"Content-Type: application/octet-stream\r\n" + f"Transfer-Encoding: chunked\r\n" + f"Connection: close\r\n" + f"\r\n" + ) + try: + s.connect((self._local_host, self._sagemaker_port)) + # HTTP request with chunked encoding + s.sendall((header.encode())) + + # Send chunked payload + for _ in range(chunk_count): + s.send(b"1\r\nA\r\n") + # End chunked encoding + s.sendall(b"0\r\n\r\n") + + # Receive response + response = b"" + while True: + try: + chunk = s.recv(4096) + if not chunk: + break + response += chunk + except socket.timeout: + break + self.assertIn(expected_response, response.decode()) + except Exception as e: + raise (e) + finally: + s.close() + + def test_load_model(self): + request_header = ( + f"POST /models HTTP/1.1\r\n" f"X-Amzn-SageMaker-Target-Model: ZZZZZZZ\r\n" + ) + self.send_chunked_request( + request_header, + self._malicious_chunk_count, + "failed to parse the request JSON buffer: Invalid value. at 0", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/qa/L0_sagemaker/test.sh b/qa/L0_sagemaker/test.sh index dde4c794d8..c86b278e38 100755 --- a/qa/L0_sagemaker/test.sh +++ b/qa/L0_sagemaker/test.sh @@ -565,6 +565,53 @@ kill $SERVER_PID wait $SERVE_PID # MME end +### Test Sagemaker Requests Containing Many Chunks ### +rm -rf models && mkdir models && \ + cp -r $DATADIR/qa_model_repository/onnx_int32_int32_int32 models/sm_model && \ + rm -r models/sm_model/2 && rm -r models/sm_model/3 && \ + sed -i "s/onnx_int32_int32_int32/sm_model/" models/sm_model/config.pbtxt + +export SAGEMAKER_TRITON_DEFAULT_MODEL_NAME=sm_model +REQUEST_MANY_CHUNKS_PY="sagemaker_request_many_chunks.py" +CLIENT_LOG="./client.sagemaker_request_many_chunks.log" +SERVER_LOG="./server.sagemaker_request_many_chunks.log" + +serve > $SERVER_LOG 2>&1 & +SERVE_PID=$! +# Obtain Triton PID in such way as $! will return the script PID +sleep 1 +SERVER_PID=`ps | grep tritonserver | awk '{ printf $1 }'` +sagemaker_wait_for_server_ready $SERVER_PID 10 +if [ "$WAIT_RET" != "0" ]; then + echo -e "\n***\n*** Failed to start $SERVER\n***" + kill $SERVER_PID || true + cat $SERVER_LOG + exit 1 +fi + +# Ping +set +e +code=`curl -s -w %{http_code} -o ./ping.out localhost:8080/ping` +set -e +if [ "$code" != "200" ]; then + cat ./ping.out + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi + +set +e +python $REQUEST_MANY_CHUNKS_PY >>$CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + echo -e "\n***\n*** Sagemaker Request Many Chunks Test Failed\n***" + cat $SERVER_LOG + cat $CLIENT_LOG + RET=1 +fi +set -e + +kill $SERVER_PID +wait $SERVE_PID + unlink /opt/ml/model rm -rf /opt/ml/model diff --git a/src/http_server.cc b/src/http_server.cc index e3ee3efe17..f62f73c948 100644 --- a/src/http_server.cc +++ b/src/http_server.cc @@ -1053,62 +1053,6 @@ ValidateOutputParameter(triton::common::TritonJson::Value& io) return nullptr; // success } -TRITONSERVER_Error* -EVBufferToJson( - triton::common::TritonJson::Value* document, evbuffer_iovec* v, int* v_idx, - const size_t length, int n) -{ - size_t offset = 0, remaining_length = length; - char* json_base; - std::vector json_buffer; - - // No need to memcpy when number of iovecs is 1 - if ((n > 0) && (v[0].iov_len >= remaining_length)) { - json_base = static_cast(v[0].iov_base); - if (v[0].iov_len > remaining_length) { - v[0].iov_base = static_cast(json_base + remaining_length); - v[0].iov_len -= remaining_length; - remaining_length = 0; - } else if (v[0].iov_len == remaining_length) { - remaining_length = 0; - *v_idx += 1; - } - } else { - json_buffer.resize(length); - json_base = json_buffer.data(); - while ((remaining_length > 0) && (*v_idx < n)) { - char* base = static_cast(v[*v_idx].iov_base); - size_t base_size; - if (v[*v_idx].iov_len > remaining_length) { - base_size = remaining_length; - v[*v_idx].iov_base = static_cast(base + remaining_length); - v[*v_idx].iov_len -= remaining_length; - remaining_length = 0; - } else { - base_size = v[*v_idx].iov_len; - remaining_length -= v[*v_idx].iov_len; - *v_idx += 1; - } - - memcpy(json_base + offset, base, base_size); - offset += base_size; - } - } - - if (remaining_length != 0) { - return TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INVALID_ARG, - std::string( - "unexpected size for request JSON, expecting " + - std::to_string(remaining_length) + " more bytes") - .c_str()); - } - - RETURN_IF_ERR(document->Parse(json_base, length)); - - return nullptr; // success -} - std::string CompressionTypeUsed(const std::string accept_encoding) { @@ -1432,34 +1376,16 @@ HTTPAPIServer::HandleRepositoryIndex( } TRITONSERVER_Error* err = nullptr; - - struct evbuffer_iovec* v = nullptr; - int v_idx = 0; - int n = evbuffer_peek(req->buffer_in, -1, NULL, NULL, 0); - if (n > 0) { - v = static_cast( - alloca(sizeof(struct evbuffer_iovec) * n)); - if (evbuffer_peek(req->buffer_in, -1, NULL, v, n) != n) { - err = TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INTERNAL, - "unexpected error getting registry index request body"); - } - } - + triton::common::TritonJson::Value index_request; bool ready = false; + size_t buffer_len = 0; + RETURN_AND_RESPOND_IF_ERR( + req, EVRequestToJson(req, "registry index", &index_request, &buffer_len)); - if (err == nullptr) { - // If no request json then just use all default values. - size_t buffer_len = evbuffer_get_length(req->buffer_in); - if (buffer_len > 0) { - triton::common::TritonJson::Value index_request; - err = EVBufferToJson(&index_request, v, &v_idx, buffer_len, n); - if (err == nullptr) { - triton::common::TritonJson::Value ready_json; - if (index_request.Find("ready", &ready_json)) { - err = ready_json.AsBool(&ready); - } - } + if (buffer_len > 0) { + triton::common::TritonJson::Value ready_json; + if (index_request.Find("ready", &ready_json)) { + err = ready_json.AsBool(&ready); } } @@ -1508,19 +1434,6 @@ HTTPAPIServer::HandleRepositoryControl( "'repository_name' specification is not supported"); } else { if (action == "load") { - struct evbuffer_iovec* v = nullptr; - int v_idx = 0; - int n = evbuffer_peek(req->buffer_in, -1, NULL, NULL, 0); - if (n > 0) { - v = static_cast( - alloca(sizeof(struct evbuffer_iovec) * n)); - if (evbuffer_peek(req->buffer_in, -1, NULL, v, n) != n) { - RETURN_AND_RESPOND_IF_ERR( - req, TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INTERNAL, - "unexpected error getting load model request buffers")); - } - } static auto param_deleter = [](std::vector* params) { if (params != nullptr) { @@ -1538,15 +1451,15 @@ HTTPAPIServer::HandleRepositoryControl( std::list> binary_files; // WAR for the const-ness check std::vector const_params; - size_t buffer_len = evbuffer_get_length(req->buffer_in); - if (buffer_len > 0) { - triton::common::TritonJson::Value request; - RETURN_AND_RESPOND_IF_ERR( - req, EVBufferToJson(&request, v, &v_idx, buffer_len, n)); + triton::common::TritonJson::Value load_request; + size_t buffer_len = 0; + RETURN_AND_RESPOND_IF_ERR( + req, EVRequestToJson(req, "load model", &load_request, &buffer_len)); + if (buffer_len > 0) { // Parse request body for parameters triton::common::TritonJson::Value param_json; - if (request.Find("parameters", ¶m_json)) { + if (load_request.Find("parameters", ¶m_json)) { // Iterate over each member in 'param_json' std::vector members; RETURN_AND_RESPOND_IF_ERR(req, param_json.Members(&members)); @@ -1592,37 +1505,25 @@ HTTPAPIServer::HandleRepositoryControl( // Check if the dependent models should be removed bool unload_dependents = false; { - struct evbuffer_iovec* v = nullptr; - int v_idx = 0; - int n = evbuffer_peek(req->buffer_in, -1, NULL, NULL, 0); - if (n > 0) { - v = static_cast( - alloca(sizeof(struct evbuffer_iovec) * n)); - if (evbuffer_peek(req->buffer_in, -1, NULL, v, n) != n) { - err = TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INTERNAL, - "unexpected error getting model control request body"); - } - } + triton::common::TritonJson::Value control_request; + size_t buffer_len = 0; + RETURN_AND_RESPOND_IF_ERR( + req, EVRequestToJson( + req, "unload model", &control_request, &buffer_len)); - size_t buffer_len = evbuffer_get_length(req->buffer_in); if (buffer_len > 0) { - triton::common::TritonJson::Value control_request; - err = EVBufferToJson(&control_request, v, &v_idx, buffer_len, n); - if (err == nullptr) { - triton::common::TritonJson::Value params_json; - if (control_request.Find("parameters", ¶ms_json)) { - triton::common::TritonJson::Value ud_json; - if (params_json.Find("unload_dependents", &ud_json)) { - auto parse_err = ud_json.AsBool(&unload_dependents); - if (parse_err != nullptr) { - err = TRITONSERVER_ErrorNew( - TRITONSERVER_ErrorCode(parse_err), - (std::string("Unable to parse 'unload_dependents': ") + - TRITONSERVER_ErrorMessage(parse_err)) - .c_str()); - TRITONSERVER_ErrorDelete(parse_err); - } + triton::common::TritonJson::Value params_json; + if (control_request.Find("parameters", ¶ms_json)) { + triton::common::TritonJson::Value ud_json; + if (params_json.Find("unload_dependents", &ud_json)) { + auto parse_err = ud_json.AsBool(&unload_dependents); + if (parse_err != nullptr) { + err = TRITONSERVER_ErrorNew( + TRITONSERVER_ErrorCode(parse_err), + (std::string("Unable to parse 'unload_dependents': ") + + TRITONSERVER_ErrorMessage(parse_err)) + .c_str()); + TRITONSERVER_ErrorDelete(parse_err); } } } @@ -1868,42 +1769,27 @@ HTTPAPIServer::HandleTrace(evhtp_request_t* req, const std::string& model_name) // Perform trace setting update if requested if (req->method == htp_method_POST) { - struct evbuffer_iovec* v = nullptr; - int v_idx = 0; - int n = evbuffer_peek(req->buffer_in, -1, NULL, NULL, 0); - if (n > 0) { - v = static_cast( - alloca(sizeof(struct evbuffer_iovec) * n)); - if (evbuffer_peek(req->buffer_in, -1, NULL, v, n) != n) { - RETURN_AND_RESPOND_IF_ERR( - req, TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INTERNAL, - "unexpected error getting trace request buffers")); - } - } - - triton::common::TritonJson::Value request; - size_t buffer_len = evbuffer_get_length(req->buffer_in); + triton::common::TritonJson::Value trace_request; RETURN_AND_RESPOND_IF_ERR( - req, EVBufferToJson(&request, v, &v_idx, buffer_len, n)); + req, EVRequestToJsonAllowsEmpty(req, "trace", &trace_request)); TraceManager::NewSetting new_setting; triton::common::TritonJson::Value setting_json; - if (request.Find("trace_file", &setting_json)) { + if (trace_request.Find("trace_file", &setting_json)) { RETURN_AND_RESPOND_IF_ERR( req, TRITONSERVER_ErrorNew( TRITONSERVER_ERROR_UNSUPPORTED, "trace file location can not be updated through network " "protocol")); } - if (request.Find("trace_level", &setting_json)) { + if (trace_request.Find("trace_level", &setting_json)) { if (setting_json.IsNull()) { new_setting.clear_level_ = true; } else { triton::common::TritonJson::Value level_array; RETURN_AND_RESPOND_IF_ERR( - req, request.MemberAsArray("trace_level", &level_array)); + req, trace_request.MemberAsArray("trace_level", &level_array)); for (size_t i = 0; i < level_array.ArraySize(); ++i) { std::string level_str; RETURN_AND_RESPOND_IF_ERR( @@ -1930,7 +1816,7 @@ HTTPAPIServer::HandleTrace(evhtp_request_t* req, const std::string& model_name) } } } - if (request.Find("trace_rate", &setting_json)) { + if (trace_request.Find("trace_rate", &setting_json)) { if (setting_json.IsNull()) { new_setting.clear_rate_ = true; } else { @@ -1963,7 +1849,7 @@ HTTPAPIServer::HandleTrace(evhtp_request_t* req, const std::string& model_name) } } } - if (request.Find("trace_count", &setting_json)) { + if (trace_request.Find("trace_count", &setting_json)) { if (setting_json.IsNull()) { new_setting.clear_count_ = true; } else { @@ -2005,7 +1891,7 @@ HTTPAPIServer::HandleTrace(evhtp_request_t* req, const std::string& model_name) } } } - if (request.Find("log_frequency", &setting_json)) { + if (trace_request.Find("log_frequency", &setting_json)) { if (setting_json.IsNull()) { new_setting.clear_log_frequency_ = true; } else { @@ -2128,28 +2014,13 @@ HTTPAPIServer::HandleLogging(evhtp_request_t* req) #ifdef TRITON_ENABLE_LOGGING // Perform log setting update if requested if (req->method == htp_method_POST) { - struct evbuffer_iovec* v = nullptr; - int v_idx = 0; - int n = evbuffer_peek(req->buffer_in, -1, NULL, NULL, 0); - if (n > 0) { - v = static_cast( - alloca(sizeof(struct evbuffer_iovec) * n)); - if (evbuffer_peek(req->buffer_in, -1, NULL, v, n) != n) { - RETURN_AND_RESPOND_IF_ERR( - req, - TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INTERNAL, - "unexpected error getting dynamic logging request buffers")); - } - } - triton::common::TritonJson::Value request; - size_t buffer_len = evbuffer_get_length(req->buffer_in); + triton::common::TritonJson::Value log_request; RETURN_AND_RESPOND_IF_ERR( - req, EVBufferToJson(&request, v, &v_idx, buffer_len, n)); + req, EVRequestToJsonAllowsEmpty(req, "dynamic logging", &log_request)); // Server and Core repos do not have the same Logger object // Each update must be applied to both server and core repo versions triton::common::TritonJson::Value setting_json; - if (request.Find("log_file", &setting_json)) { + if (log_request.Find("log_file", &setting_json)) { if (!setting_json.IsNull()) { RETURN_AND_RESPOND_IF_ERR( req, TRITONSERVER_ErrorNew( @@ -2158,7 +2029,7 @@ HTTPAPIServer::HandleLogging(evhtp_request_t* req) "protocol")); } } - if (request.Find("log_info", &setting_json)) { + if (log_request.Find("log_info", &setting_json)) { if (!setting_json.IsNull()) { bool log_info_status; RETURN_AND_RESPOND_IF_ERR(req, setting_json.AsBool(&log_info_status)); @@ -2166,7 +2037,7 @@ HTTPAPIServer::HandleLogging(evhtp_request_t* req) TRITONSERVER_ServerOptionsSetLogInfo(nullptr, log_info_status); } } - if (request.Find("log_warning", &setting_json)) { + if (log_request.Find("log_warning", &setting_json)) { if (!setting_json.IsNull()) { bool log_warn_status; RETURN_AND_RESPOND_IF_ERR(req, setting_json.AsBool(&log_warn_status)); @@ -2174,7 +2045,7 @@ HTTPAPIServer::HandleLogging(evhtp_request_t* req) TRITONSERVER_ServerOptionsSetLogWarn(nullptr, log_warn_status); } } - if (request.Find("log_error", &setting_json)) { + if (log_request.Find("log_error", &setting_json)) { if (!setting_json.IsNull()) { bool log_error_status; RETURN_AND_RESPOND_IF_ERR(req, setting_json.AsBool(&log_error_status)); @@ -2182,7 +2053,7 @@ HTTPAPIServer::HandleLogging(evhtp_request_t* req) TRITONSERVER_ServerOptionsSetLogError(nullptr, log_error_status); } } - if (request.Find("log_verbose_level", &setting_json)) { + if (log_request.Find("log_verbose_level", &setting_json)) { if (!setting_json.IsNull()) { uint64_t verbose_level; RETURN_AND_RESPOND_IF_ERR(req, setting_json.AsUInt(&verbose_level)); @@ -2191,7 +2062,7 @@ HTTPAPIServer::HandleLogging(evhtp_request_t* req) nullptr, static_cast(verbose_level)); } } - if (request.Find("log_format", &setting_json)) { + if (log_request.Find("log_format", &setting_json)) { if (!setting_json.IsNull()) { std::string log_format_parse; RETURN_AND_RESPOND_IF_ERR( @@ -2310,63 +2181,46 @@ HTTPAPIServer::HandleSystemSharedMemory( TRITONSERVER_ERROR_INVALID_ARG, "'region name' is necessary to register system shared memory region"); } else { - struct evbuffer_iovec* v = nullptr; - int v_idx = 0; - int n = evbuffer_peek(req->buffer_in, -1, NULL, NULL, 0); - if (n > 0) { - v = static_cast( - alloca(sizeof(struct evbuffer_iovec) * n)); - if (evbuffer_peek(req->buffer_in, -1, NULL, v, n) != n) { - err = TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INTERNAL, - "unexpected error getting register request buffers"); - } + triton::common::TritonJson::Value register_request; + triton::common::TritonJson::Value key_json; + RETURN_AND_RESPOND_IF_ERR( + req, EVRequestToJsonAllowsEmpty(req, action, ®ister_request)); + if (!register_request.Find("key", &key_json)) { + err = TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + "Shared memory register request has no 'key' field"); } + const char* shm_key = nullptr; if (err == nullptr) { - triton::common::TritonJson::Value register_request; - size_t buffer_len = evbuffer_get_length(req->buffer_in); - err = EVBufferToJson(®ister_request, v, &v_idx, buffer_len, n); - if (err == nullptr) { - triton::common::TritonJson::Value key_json; - if (!register_request.Find("key", &key_json)) { - err = TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INVALID_ARG, - "Shared memory register request has no 'key' field"); - } - - const char* shm_key = nullptr; - if (err == nullptr) { - size_t shm_key_len; - err = key_json.AsString(&shm_key, &shm_key_len); - } - - uint64_t offset = 0; - if (err == nullptr) { - triton::common::TritonJson::Value offset_json; - if (register_request.Find("offset", &offset_json)) { - err = offset_json.AsUInt(&offset); - } - } + size_t shm_key_len; + err = key_json.AsString(&shm_key, &shm_key_len); + } - uint64_t byte_size = 0; - if (err == nullptr) { - triton::common::TritonJson::Value byte_size_json; - if (!register_request.Find("byte_size", &byte_size_json)) { - err = TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INVALID_ARG, - "Shared memory register request has no 'byte_size' field"); - } else { - err = byte_size_json.AsUInt(&byte_size); - } - } + uint64_t offset = 0; + if (err == nullptr) { + triton::common::TritonJson::Value offset_json; + if (register_request.Find("offset", &offset_json)) { + err = offset_json.AsUInt(&offset); + } + } - if (err == nullptr) { - err = shm_manager_->RegisterSystemSharedMemory( - region_name, shm_key, offset, byte_size); - } + uint64_t byte_size = 0; + if (err == nullptr) { + triton::common::TritonJson::Value byte_size_json; + if (!register_request.Find("byte_size", &byte_size_json)) { + err = TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + "Shared memory register request has no 'byte_size' field"); + } else { + err = byte_size_json.AsUInt(&byte_size); } } + + if (err == nullptr) { + err = shm_manager_->RegisterSystemSharedMemory( + region_name, shm_key, offset, byte_size); + } } } else if (action == "unregister") { if (region_name.empty()) { @@ -2417,67 +2271,50 @@ HTTPAPIServer::HandleCudaSharedMemory( "'region name' is necessary to register cuda shared memory region"); } else { #ifdef TRITON_ENABLE_GPU - struct evbuffer_iovec* v = nullptr; - int v_idx = 0; - int n = evbuffer_peek(req->buffer_in, -1, NULL, NULL, 0); - if (n > 0) { - v = static_cast( - alloca(sizeof(struct evbuffer_iovec) * n)); - if (evbuffer_peek(req->buffer_in, -1, NULL, v, n) != n) { - err = TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INTERNAL, - "unexpected error getting register request buffers"); - } + triton::common::TritonJson::Value register_request; + RETURN_AND_RESPOND_IF_ERR( + req, EVRequestToJsonAllowsEmpty(req, action, ®ister_request)); + const char* b64_handle = nullptr; + size_t b64_handle_len = 0; + triton::common::TritonJson::Value raw_handle_json; + if (!register_request.Find("raw_handle", &raw_handle_json)) { + err = TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + "Shared memory register request has no 'raw_handle' field"); + } else { + err = + raw_handle_json.MemberAsString("b64", &b64_handle, &b64_handle_len); } - if (err == nullptr) { - triton::common::TritonJson::Value register_request; - size_t buffer_len = evbuffer_get_length(req->buffer_in); - err = EVBufferToJson(®ister_request, v, &v_idx, buffer_len, n); - if (err == nullptr) { - const char* b64_handle = nullptr; - size_t b64_handle_len = 0; - triton::common::TritonJson::Value raw_handle_json; - if (!register_request.Find("raw_handle", &raw_handle_json)) { - err = TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INVALID_ARG, - "Shared memory register request has no 'raw_handle' field"); - } else { - err = raw_handle_json.MemberAsString( - "b64", &b64_handle, &b64_handle_len); - } - uint64_t byte_size = 0; - if (err == nullptr) { - err = register_request.MemberAsUInt("byte_size", &byte_size); - } + uint64_t byte_size = 0; + if (err == nullptr) { + err = register_request.MemberAsUInt("byte_size", &byte_size); + } - uint64_t device_id = 0; - if (err == nullptr) { - err = register_request.MemberAsUInt("device_id", &device_id); - } + uint64_t device_id = 0; + if (err == nullptr) { + err = register_request.MemberAsUInt("device_id", &device_id); + } - if (err == nullptr) { - size_t decoded_size; - std::vector raw_handle; - RETURN_AND_RESPOND_IF_ERR( - req, DecodeBase64( - b64_handle, b64_handle_len, raw_handle, decoded_size, - "raw_handle")); + if (err == nullptr) { + size_t decoded_size; + std::vector raw_handle; + RETURN_AND_RESPOND_IF_ERR( + req, DecodeBase64( + b64_handle, b64_handle_len, raw_handle, decoded_size, + "raw_handle")); - if (decoded_size != sizeof(cudaIpcMemHandle_t)) { - err = TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INVALID_ARG, - "'raw_handle' must be a valid base64 encoded " - "cudaIpcMemHandle_t"); - } else { - raw_handle.resize(sizeof(cudaIpcMemHandle_t)); - err = shm_manager_->RegisterCUDASharedMemory( - region_name.c_str(), - reinterpret_cast( - raw_handle.data()), - byte_size, device_id); - } - } + if (decoded_size != sizeof(cudaIpcMemHandle_t)) { + err = TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + "'raw_handle' must be a valid base64 encoded " + "cudaIpcMemHandle_t"); + } else { + raw_handle.resize(sizeof(cudaIpcMemHandle_t)); + err = shm_manager_->RegisterCUDASharedMemory( + region_name.c_str(), + reinterpret_cast(raw_handle.data()), + byte_size, device_id); } } #else @@ -3028,25 +2865,50 @@ HTTPAPIServer::ParseJsonTritonRequestID( return nullptr; // Success } -// TODO: Can refactor other non-inference routes to re-use this helper instead. TRITONSERVER_Error* -HTTPAPIServer::EVRequestToJson( - evhtp_request_t* req, triton::common::TritonJson::Value* request_json_ptr) +HTTPAPIServer::EVRequestToJsonImpl( + evhtp_request_t* req, std::string_view request_kind, bool allows_empty_body, + triton::common::TritonJson::Value* request_json, size_t* buffer_len) { struct evbuffer_iovec* v = nullptr; int v_idx = 0; + std::vector v_vec; + int n = evbuffer_peek(req->buffer_in, -1, NULL, NULL, 0); if (n > 0) { - v = static_cast( - alloca(sizeof(struct evbuffer_iovec) * n)); + try { + v_vec = std::vector(n); + } + catch (const std::bad_alloc& e) { + // Handle memory allocation failure + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + (std::string("Memory allocation failed for evbuffer: ") + e.what()) + .c_str()); + } + catch (const std::exception& e) { + // Catch any other std exceptions + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INTERNAL, + (std::string("Exception while creating evbuffer vector: ") + e.what()) + .c_str()); + } + + v = v_vec.data(); if (evbuffer_peek(req->buffer_in, -1, NULL, v, n) != n) { return TRITONSERVER_ErrorNew( TRITONSERVER_ERROR_INTERNAL, - "Unexpected error getting request buffers"); + ("Unexpected error getting " + std::string(request_kind) + + " request buffers") + .c_str()); } } - size_t buffer_len = evbuffer_get_length(req->buffer_in); - RETURN_IF_ERR(EVBufferToJson(request_json_ptr, v, &v_idx, buffer_len, n)); + + *buffer_len = evbuffer_get_length(req->buffer_in); + if (allows_empty_body || *buffer_len > 0) { + RETURN_IF_ERR(EVBufferToJson(request_json, v, &v_idx, *buffer_len, n)); + } + return nullptr; // success } @@ -3063,11 +2925,29 @@ HTTPAPIServer::EVBufferToInput( // body. struct evbuffer_iovec* v = nullptr; int v_idx = 0; + std::vector v_vec; int n = evbuffer_peek(input_buffer, -1, NULL, NULL, 0); if (n > 0) { - v = static_cast( - alloca(sizeof(struct evbuffer_iovec) * n)); + try { + v_vec = std::vector(n); + } + catch (const std::bad_alloc& e) { + // Handle memory allocation failure + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + (std::string("Memory allocation failed for evbuffer: ") + e.what()) + .c_str()); + } + catch (const std::exception& e) { + // Catch any other std exceptions + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INTERNAL, + (std::string("Exception while creating evbuffer vector: ") + e.what()) + .c_str()); + } + + v = v_vec.data(); if (evbuffer_peek(input_buffer, -1, NULL, v, n) != n) { return TRITONSERVER_ErrorNew( TRITONSERVER_ERROR_INTERNAL, @@ -3120,10 +3000,30 @@ HTTPAPIServer::EVBufferToRawInput( } else { struct evbuffer_iovec* v = nullptr; int v_idx = 0; + std::vector v_vec; + int n = evbuffer_peek(input_buffer, -1, NULL, NULL, 0); if (n > 0) { - v = static_cast( - alloca(sizeof(struct evbuffer_iovec) * n)); + try { + v_vec = std::vector(n); + } + catch (const std::bad_alloc& e) { + // Handle memory allocation failure + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + (std::string("Memory allocation failed for evbuffer: ") + e.what()) + .c_str()); + } + catch (const std::exception& e) { + // Catch any other std exceptions + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INTERNAL, + (std::string("Exception while creating evbuffer vector: ") + + e.what()) + .c_str()); + } + + v = v_vec.data(); if (evbuffer_peek(input_buffer, -1, NULL, v, n) != n) { return TRITONSERVER_ErrorNew( TRITONSERVER_ERROR_INTERNAL, @@ -3155,6 +3055,62 @@ HTTPAPIServer::EVBufferToRawInput( return nullptr; // success } +TRITONSERVER_Error* +HTTPAPIServer::EVBufferToJson( + triton::common::TritonJson::Value* document, evbuffer_iovec* v, int* v_idx, + const size_t length, int n) +{ + size_t offset = 0, remaining_length = length; + char* json_base; + std::vector json_buffer; + + // No need to memcpy when number of iovecs is 1 + if ((n > 0) && (v[0].iov_len >= remaining_length)) { + json_base = static_cast(v[0].iov_base); + if (v[0].iov_len > remaining_length) { + v[0].iov_base = static_cast(json_base + remaining_length); + v[0].iov_len -= remaining_length; + remaining_length = 0; + } else if (v[0].iov_len == remaining_length) { + remaining_length = 0; + *v_idx += 1; + } + } else { + json_buffer.resize(length); + json_base = json_buffer.data(); + while ((remaining_length > 0) && (*v_idx < n)) { + char* base = static_cast(v[*v_idx].iov_base); + size_t base_size; + if (v[*v_idx].iov_len > remaining_length) { + base_size = remaining_length; + v[*v_idx].iov_base = static_cast(base + remaining_length); + v[*v_idx].iov_len -= remaining_length; + remaining_length = 0; + } else { + base_size = v[*v_idx].iov_len; + remaining_length -= v[*v_idx].iov_len; + *v_idx += 1; + } + + memcpy(json_base + offset, base, base_size); + offset += base_size; + } + } + + if (remaining_length != 0) { + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + std::string( + "unexpected size for request JSON, expecting " + + std::to_string(remaining_length) + " more bytes") + .c_str()); + } + + RETURN_IF_ERR(document->Parse(json_base, length)); + + return nullptr; // success +} + struct HeaderSearchPayload { HeaderSearchPayload( const re2::RE2& regex, TRITONSERVER_InferenceRequest* request) @@ -3429,7 +3385,8 @@ HTTPAPIServer::HandleGenerate( // arbitrary JSON message convoluted (added key is reference to a string and // thus the string must live as long as the JSON message). triton::common::TritonJson::Value request; - RETURN_AND_CALLBACK_IF_ERR(EVRequestToJson(req, &request), error_callback); + RETURN_AND_CALLBACK_IF_ERR( + EVRequestToJsonAllowsEmpty(req, "generate", &request), error_callback); RETURN_AND_CALLBACK_IF_ERR( ParseJsonTritonRequestID(request, irequest), error_callback); diff --git a/src/http_server.h b/src/http_server.h index 877327b4d5..c5ed0eb6de 100644 --- a/src/http_server.h +++ b/src/http_server.h @@ -607,9 +607,34 @@ class HTTPAPIServer : public HTTPServer { std::map* input_metadata, triton::common::TritonJson::Value* meta_data_root); + // Internal utility method for parsing evhtp request to JSON + // Should not be called directly - use EVRequestToJson or + // EVRequestToJsonAllowsEmpty instead + TRITONSERVER_Error* EVRequestToJsonImpl( + evhtp_request_t* req, std::string_view request_kind, + bool allows_empty_body, triton::common::TritonJson::Value* request_json, + size_t* buffer_len); + // Parses full evhtp request and its evbuffers into JSON. + TRITONSERVER_Error* EVRequestToJsonAllowsEmpty( + evhtp_request_t* req, std::string_view request_kind, + triton::common::TritonJson::Value* request_json) + { + size_t buffer_len = 0; + TRITONSERVER_Error* err = + EVRequestToJsonImpl(req, request_kind, true, request_json, &buffer_len); + return err; + } + TRITONSERVER_Error* EVRequestToJson( - evhtp_request_t* req, triton::common::TritonJson::Value* request_json); + evhtp_request_t* req, std::string_view request_kind, + triton::common::TritonJson::Value* request_json, size_t* buffer_len) + { + TRITONSERVER_Error* err = + EVRequestToJsonImpl(req, request_kind, false, request_json, buffer_len); + return err; + } + // Parses evhtp request buffers into Triton Inference Request. TRITONSERVER_Error* EVRequestToTritonRequest( evhtp_request_t* req, const std::string& model_name, @@ -622,6 +647,9 @@ class HTTPAPIServer : public HTTPServer { TRITONSERVER_Error* EVBufferToRawInput( const std::string& model_name, TRITONSERVER_InferenceRequest* irequest, evbuffer* input_buffer, InferRequestClass* infer_req); + TRITONSERVER_Error* EVBufferToJson( + triton::common::TritonJson::Value* document, evbuffer_iovec* v, + int* v_idx, const size_t length, int n); // Helpers for parsing JSON requests for Triton-specific fields diff --git a/src/sagemaker_server.cc b/src/sagemaker_server.cc index 52074f2b9d..49b8a3ba06 100644 --- a/src/sagemaker_server.cc +++ b/src/sagemaker_server.cc @@ -54,63 +54,6 @@ EVBufferAddErrorJson(evbuffer* buffer, TRITONSERVER_Error* err) evbuffer_add(buffer, buffer_json.Base(), buffer_json.Size()); } - -TRITONSERVER_Error* -EVBufferToJson( - triton::common::TritonJson::Value* document, evbuffer_iovec* v, int* v_idx, - const size_t length, int n) -{ - size_t offset = 0, remaining_length = length; - char* json_base; - std::vector json_buffer; - - // No need to memcpy when number of iovecs is 1 - if ((n > 0) && (v[0].iov_len >= remaining_length)) { - json_base = static_cast(v[0].iov_base); - if (v[0].iov_len > remaining_length) { - v[0].iov_base = static_cast(json_base + remaining_length); - v[0].iov_len -= remaining_length; - remaining_length = 0; - } else if (v[0].iov_len == remaining_length) { - remaining_length = 0; - *v_idx += 1; - } - } else { - json_buffer.resize(length); - json_base = json_buffer.data(); - while ((remaining_length > 0) && (*v_idx < n)) { - char* base = static_cast(v[*v_idx].iov_base); - size_t base_size; - if (v[*v_idx].iov_len > remaining_length) { - base_size = remaining_length; - v[*v_idx].iov_base = static_cast(base + remaining_length); - v[*v_idx].iov_len -= remaining_length; - remaining_length = 0; - } else { - base_size = v[*v_idx].iov_len; - remaining_length -= v[*v_idx].iov_len; - *v_idx += 1; - } - - memcpy(json_base + offset, base, base_size); - offset += base_size; - } - } - - if (remaining_length != 0) { - return TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INVALID_ARG, - std::string( - "unexpected size for request JSON, expecting " + - std::to_string(remaining_length) + " more bytes") - .c_str()); - } - - RETURN_IF_ERR(document->Parse(json_base, length)); - - return nullptr; // success -} - } // namespace @@ -266,7 +209,9 @@ SagemakerAPIServer::Handle(evhtp_request_t* req) std::unordered_map parse_load_map; ParseSageMakerRequest(req, &parse_load_map, "load"); - SageMakerMMELoadModel(req, parse_load_map); + if (!parse_load_map.empty()) { + SageMakerMMELoadModel(req, parse_load_map); + } return; } break; @@ -320,29 +265,15 @@ SagemakerAPIServer::ParseSageMakerRequest( std::unordered_map* parse_map, const std::string& action) { - struct evbuffer_iovec* v = nullptr; - int v_idx = 0; - int n = evbuffer_peek(req->buffer_in, -1, NULL, NULL, 0); - if (n > 0) { - v = static_cast( - alloca(sizeof(struct evbuffer_iovec) * n)); - if (evbuffer_peek(req->buffer_in, -1, NULL, v, n) != n) { - HTTP_RESPOND_IF_ERR( - req, TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INTERNAL, - "unexpected error getting load model request buffers")); - } - } + size_t buffer_len; + triton::common::TritonJson::Value request; + HTTP_RESPOND_IF_ERR( + req, EVRequestToJson(req, "load model", &request, &buffer_len)); std::string model_name_string; std::string url_string; - size_t buffer_len = evbuffer_get_length(req->buffer_in); if (buffer_len > 0) { - triton::common::TritonJson::Value request; - HTTP_RESPOND_IF_ERR( - req, EVBufferToJson(&request, v, &v_idx, buffer_len, n)); - triton::common::TritonJson::Value url; triton::common::TritonJson::Value model_name; From 99f4820bc9786c9702af0d78c3dc5e4db53c69f4 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Tue, 29 Jul 2025 03:35:56 -0700 Subject: [PATCH 060/230] ci: Anchor datacenter-gpu-manager-4-core version (#8315) Co-authored-by: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> --- Dockerfile.sdk | 5 ++++- build.py | 24 ++++++++++++++++-------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/Dockerfile.sdk b/Dockerfile.sdk index 7d114e2ce5..8fbc8cd3c0 100644 --- a/Dockerfile.sdk +++ b/Dockerfile.sdk @@ -265,7 +265,10 @@ RUN if [ "$TRITON_ENABLE_GPU" = "ON" ]; then \ curl -o /tmp/cuda-keyring.deb \ https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/$arch/cuda-keyring_1.1-1_all.deb \ && apt install /tmp/cuda-keyring.deb && rm /tmp/cuda-keyring.deb && \ - apt update && apt install --yes datacenter-gpu-manager-4-dev=1:${DCGM_VERSION}; \ + apt update && \ + apt install --yes --no-install-recommends \ + datacenter-gpu-manager-4-core=1:${DCGM_VERSION} \ + datacenter-gpu-manager-4-dev=1:${DCGM_VERSION}; \ fi # Build expects "python" executable (not python3). diff --git a/build.py b/build.py index 230b4dc88c..6c726e6c87 100755 --- a/build.py +++ b/build.py @@ -858,9 +858,11 @@ def install_dcgm_libraries(dcgm_version, target_machine): # Install DCGM. Steps from https://developer.nvidia.com/dcgm#Downloads RUN dnf config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel8/sbsa/cuda-rhel8.repo \\ && dnf clean expire-cache \\ - && dnf install --assumeyes datacenter-gpu-manager-4-devel=1:{} + && dnf install --assumeyes \\ + datacenter-gpu-manager-4-core=1:{} \\ + datacenter-gpu-manager-4-devel=1:{} """.format( - dcgm_version, dcgm_version + dcgm_version, dcgm_version, dcgm_version ) else: return """ @@ -868,9 +870,11 @@ def install_dcgm_libraries(dcgm_version, target_machine): # Install DCGM. Steps from https://developer.nvidia.com/dcgm#Downloads RUN dnf config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel8/x86_64/cuda-rhel8.repo \\ && dnf clean expire-cache \\ - && dnf install --assumeyes datacenter-gpu-manager-4-devel=1:{} + && dnf install --assumeyes \\ + datacenter-gpu-manager-4-core=1:{} \\ + datacenter-gpu-manager-4-devel=1:{} """.format( - dcgm_version, dcgm_version + dcgm_version, dcgm_version, dcgm_version ) else: if target_machine == "aarch64": @@ -882,9 +886,11 @@ def install_dcgm_libraries(dcgm_version, target_machine): && apt install /tmp/cuda-keyring.deb \\ && rm /tmp/cuda-keyring.deb \\ && apt update \\ - && apt install --yes datacenter-gpu-manager-4-dev=1:{} + && apt install --yes --no-install-recommends \\ + datacenter-gpu-manager-4-core=1:{} \\ + datacenter-gpu-manager-4-dev=1:{} """.format( - dcgm_version, dcgm_version + dcgm_version, dcgm_version, dcgm_version ) else: return """ @@ -895,9 +901,11 @@ def install_dcgm_libraries(dcgm_version, target_machine): && apt install /tmp/cuda-keyring.deb \\ && rm /tmp/cuda-keyring.deb \\ && apt update \\ - && apt install --yes datacenter-gpu-manager-4-dev=1:{} + && apt install --yes --no-install-recommends \\ + datacenter-gpu-manager-4-core=1:{} \\ + datacenter-gpu-manager-4-dev=1:{} """.format( - dcgm_version, dcgm_version + dcgm_version, dcgm_version, dcgm_version ) From e672c28586265dc7d77ded8474285db489c5a137 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Tue, 29 Jul 2025 19:13:47 -0700 Subject: [PATCH 061/230] Update default branch post 25.07 (#8319) Co-authored-by: Yingge He <157551214+yinggeh@users.noreply.github.com> Co-authored-by: Sai Kiran Polisetty --- Dockerfile.sdk | 48 ++++++++++--------- README.md | 10 ++-- build.py | 4 +- deploy/aws/values.yaml | 2 +- deploy/fleetcommand/Chart.yaml | 2 +- deploy/fleetcommand/values.yaml | 6 +-- deploy/gcp/values.yaml | 2 +- .../perf-analyzer-script/triton_client.yaml | 2 +- .../server-deployer/build_and_push.sh | 4 +- .../server-deployer/chart/triton/Chart.yaml | 2 +- .../server-deployer/chart/triton/values.yaml | 6 +-- .../server-deployer/data-test/schema.yaml | 2 +- .../server-deployer/schema.yaml | 4 +- .../gke-marketplace-app/trt-engine/README.md | 6 +-- deploy/k8s-onprem/values.yaml | 2 +- deploy/oci/values.yaml | 2 +- docs/introduction/compatibility.md | 1 + python/openai/README.md | 2 +- qa/common/gen_jetson_trt_models | 2 +- qa/common/gen_qa_custom_ops | 2 +- qa/common/gen_qa_model_repository | 2 +- 21 files changed, 59 insertions(+), 54 deletions(-) diff --git a/Dockerfile.sdk b/Dockerfile.sdk index 8fbc8cd3c0..f7bbf64432 100644 --- a/Dockerfile.sdk +++ b/Dockerfile.sdk @@ -29,7 +29,7 @@ # # Base image on the minimum Triton container -ARG BASE_IMAGE=nvcr.io/nvidia/tritonserver:25.06-py3-min +ARG BASE_IMAGE=nvcr.io/nvidia/tritonserver:25.07-py3-min ARG TRITON_CLIENT_REPO_SUBDIR=clientrepo ARG TRITON_PA_REPO_SUBDIR=perfanalyzerrepo @@ -60,34 +60,36 @@ ENV PIP_BREAK_SYSTEM_PACKAGES=1 CMAKE_POLICY_VERSION_MINIMUM=3.5 RUN apt-get update && \ apt-get install -y --no-install-recommends \ - ca-certificates \ - software-properties-common \ autoconf \ automake \ build-essential \ + ca-certificates \ curl \ git \ gperf \ libb64-dev \ libgoogle-perftools-dev \ - libopencv-dev \ libopencv-core-dev \ + libopencv-dev \ libssl-dev \ libtool \ + maven \ + openjdk-11-jdk \ pkg-config \ python3 \ - python3-pip \ python3-dev \ - python3-wheel \ + python3-pdfkit \ + python3-pip \ python3-setuptools \ + python3-wheel \ rapidjson-dev \ + software-properties-common \ vim \ - wget \ - python3-pdfkit \ - openjdk-11-jdk \ - maven && \ + wget && \ pip3 install --upgrade "grpcio-tools<1.68" cmake==4.0.3 +ENV CMAKE_POLICY_MINIMUM_REQUIRED=3.5 + # Build expects "python" executable (not python3). RUN rm -f /usr/bin/python && \ ln -s /usr/bin/python3 /usr/bin/python @@ -126,8 +128,7 @@ RUN cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ -DTRITON_ENABLE_JAVA_HTTP=ON \ -DTRITON_ENABLE_EXAMPLES=ON -DTRITON_ENABLE_TESTS=ON \ -DTRITON_ENABLE_GPU=${TRITON_ENABLE_GPU} /workspace/client -RUN make -j16 cc-clients java-clients && \ - rm -fr ~/.m2 +RUN cmake --build . -v --parallel --target cc-clients java-clients # TODO: PA will rebuild the CC clients since it depends on it. # This should be optimized so that we do not have to build @@ -145,6 +146,7 @@ RUN if [ "$TRITON_PERF_ANALYZER_BUILD" = "1" ]; then \ -DTRITON_COMMON_REPO_TAG=${TRITON_COMMON_REPO_TAG} \ -DTRITON_CORE_REPO_TAG=${TRITON_CORE_REPO_TAG} \ -DTRITON_CLIENT_REPO_TAG=${TRITON_CLIENT_REPO_TAG} \ + -DTRITON_THIRD_PARTY_REPO_TAG=${TRITON_THIRD_PARTY_REPO_TAG} \ -DTRITON_ENABLE_PERF_ANALYZER_C_API=ON \ -DTRITON_ENABLE_PERF_ANALYZER_TFS=ON \ -DTRITON_ENABLE_PERF_ANALYZER_TS=ON \ @@ -156,7 +158,7 @@ RUN if [ "$TRITON_PERF_ANALYZER_BUILD" = "1" ]; then \ -DTRITON_PACKAGE_PERF_ANALYZER=ON \ -DTRITON_ENABLE_GPU=${TRITON_ENABLE_GPU} \ /workspace/perf_analyzer && \ - make -j16 perf-analyzer python-clients && \ + cmake --build . -v --parallel --target perf-analyzer python-clients && \ pip3 install build && \ cd /workspace/perf_analyzer/genai-perf && \ python3 -m build --wheel --outdir /workspace/install/python; \ @@ -169,12 +171,13 @@ RUN if [ "$TRITON_PERF_ANALYZER_BUILD" = "1" ]; then \ -DTRITON_REPO_ORGANIZATION=${TRITON_REPO_ORGANIZATION} \ -DTRITON_COMMON_REPO_TAG=${TRITON_COMMON_REPO_TAG} \ -DTRITON_CLIENT_REPO_TAG=${TRITON_CLIENT_REPO_TAG} \ + -DTRITON_THIRD_PARTY_REPO_TAG=${TRITON_THIRD_PARTY_REPO_TAG} \ -DTRITON_ENABLE_PYTHON_HTTP=ON \ -DTRITON_ENABLE_PYTHON_GRPC=ON \ -DTRITON_PACKAGE_PERF_ANALYZER=ON \ -DTRITON_ENABLE_GPU=${TRITON_ENABLE_GPU} \ /workspace/perf_analyzer && \ - make -j16 python-clients && \ + cmake --build . -v --parallel --target python-clients && \ mkdir -p /workspace/install/python && \ cp /workspace/perf_analyzer/genai_perf-*.whl /workspace/install/python/; \ fi @@ -205,26 +208,27 @@ ARG TRITON_ENABLE_GPU RUN apt-get update && \ apt-get install -y --no-install-recommends \ - software-properties-common \ curl \ + default-jdk \ git \ gperf \ libb64-dev \ libgoogle-perftools-dev \ - libopencv-dev \ libopencv-core-dev \ + libopencv-dev \ libssl-dev \ libtool \ + maven \ + perl \ python3 \ - python3-pip \ python3-dev \ - python3-wheel \ + python3-pdfkit \ + python3-pip \ python3-setuptools \ + python3-wheel \ + software-properties-common \ vim \ - wget \ - python3-pdfkit \ - maven \ - default-jdk && \ + wget && \ pip3 install "grpcio<1.68" "grpcio-tools<1.68" WORKDIR /workspace diff --git a/README.md b/README.md index 057e58e8f9..48bd0baf1b 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ >[!WARNING] >You are currently on the `main` branch which tracks under-development progress ->towards the next release. The current release is version [2.59.0](https://github.com/triton-inference-server/server/releases/latest) ->and corresponds to the 25.06 container release on NVIDIA GPU Cloud (NGC). +>towards the next release. The current release is version [2.59.1](https://github.com/triton-inference-server/server/releases/latest) +>and corresponds to the 25.07 container release on NVIDIA GPU Cloud (NGC). # Triton Inference Server @@ -90,16 +90,16 @@ Inference Server with the ```bash # Step 1: Create the example model repository -git clone -b r25.06 https://github.com/triton-inference-server/server.git +git clone -b r25.07 https://github.com/triton-inference-server/server.git cd server/docs/examples ./fetch_models.sh # Step 2: Launch triton from the NGC Triton container -docker run --gpus=1 --rm --net=host -v ${PWD}/model_repository:/models nvcr.io/nvidia/tritonserver:25.06-py3 tritonserver --model-repository=/models --model-control-mode explicit --load-model densenet_onnx +docker run --gpus=1 --rm --net=host -v ${PWD}/model_repository:/models nvcr.io/nvidia/tritonserver:25.07-py3 tritonserver --model-repository=/models --model-control-mode explicit --load-model densenet_onnx # Step 3: Sending an Inference Request # In a separate console, launch the image_client example from the NGC Triton SDK container -docker run -it --rm --net=host nvcr.io/nvidia/tritonserver:25.06-py3-sdk /workspace/install/bin/image_client -m densenet_onnx -c 3 -s INCEPTION /workspace/images/mug.jpg +docker run -it --rm --net=host nvcr.io/nvidia/tritonserver:25.07-py3-sdk /workspace/install/bin/image_client -m densenet_onnx -c 3 -s INCEPTION /workspace/images/mug.jpg # Inference should return the following Image '/workspace/images/mug.jpg': diff --git a/build.py b/build.py index 6c726e6c87..716b227c42 100755 --- a/build.py +++ b/build.py @@ -72,8 +72,8 @@ DEFAULT_TRITON_VERSION_MAP = { "release_version": "2.60.0dev", - "triton_container_version": "25.07dev", - "upstream_container_version": "25.06", + "triton_container_version": "25.08dev", + "upstream_container_version": "25.07", "ort_version": "1.22.0", "ort_openvino_version": "2025.2.0", "standalone_openvino_version": "2025.2.0", diff --git a/deploy/aws/values.yaml b/deploy/aws/values.yaml index c24b0ed7b8..eb9afb78e0 100644 --- a/deploy/aws/values.yaml +++ b/deploy/aws/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.06-py3 + imageName: nvcr.io/nvidia/tritonserver:25.07-py3 pullPolicy: IfNotPresent modelRepositoryPath: s3://triton-inference-server-repository/model_repository numGpus: 1 diff --git a/deploy/fleetcommand/Chart.yaml b/deploy/fleetcommand/Chart.yaml index b819cbb4a5..1b1af554ee 100644 --- a/deploy/fleetcommand/Chart.yaml +++ b/deploy/fleetcommand/Chart.yaml @@ -26,7 +26,7 @@ apiVersion: v1 # appVersion is the Triton version; update when changing release -appVersion: "2.59.0" +appVersion: "2.59.1" description: Triton Inference Server (Fleet Command) name: triton-inference-server # version is the Chart version; update when changing anything in the chart diff --git a/deploy/fleetcommand/values.yaml b/deploy/fleetcommand/values.yaml index d2300ef421..03569ab343 100644 --- a/deploy/fleetcommand/values.yaml +++ b/deploy/fleetcommand/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.06-py3 + imageName: nvcr.io/nvidia/tritonserver:25.07-py3 pullPolicy: IfNotPresent numGpus: 1 serverCommand: tritonserver @@ -47,13 +47,13 @@ image: # # To set model control mode, uncomment and configure below # TODO: Fix the following url, it is invalid - # See https://github.com/triton-inference-server/server/blob/r25.06/docs/user_guide/model_management.md + # See https://github.com/triton-inference-server/server/blob/r25.07/docs/user_guide/model_management.md # for more details #- --model-control-mode=explicit|poll|none # # Additional server args # - # see https://github.com/triton-inference-server/server/blob/r25.06/README.md + # see https://github.com/triton-inference-server/server/blob/r25.07/README.md # for more details service: diff --git a/deploy/gcp/values.yaml b/deploy/gcp/values.yaml index 73948fb39d..55d0919205 100644 --- a/deploy/gcp/values.yaml +++ b/deploy/gcp/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.06-py3 + imageName: nvcr.io/nvidia/tritonserver:25.07-py3 pullPolicy: IfNotPresent modelRepositoryPath: gs://triton-inference-server-repository/model_repository numGpus: 1 diff --git a/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml b/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml index 8be23e0f2c..951f04898e 100644 --- a/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml +++ b/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml @@ -33,7 +33,7 @@ metadata: namespace: default spec: containers: - - image: nvcr.io/nvidia/tritonserver:25.06-py3-sdk + - image: nvcr.io/nvidia/tritonserver:25.07-py3-sdk imagePullPolicy: Always name: nv-triton-client securityContext: diff --git a/deploy/gke-marketplace-app/server-deployer/build_and_push.sh b/deploy/gke-marketplace-app/server-deployer/build_and_push.sh index b31804b9a2..659e3a40eb 100755 --- a/deploy/gke-marketplace-app/server-deployer/build_and_push.sh +++ b/deploy/gke-marketplace-app/server-deployer/build_and_push.sh @@ -28,8 +28,8 @@ export REGISTRY=gcr.io/$(gcloud config get-value project | tr ':' '/') export APP_NAME=tritonserver export MAJOR_VERSION=2.59 -export MINOR_VERSION=2.59.0 -export NGC_VERSION=25.06-py3 +export MINOR_VERSION=2.59.1 +export NGC_VERSION=25.07-py3 docker pull nvcr.io/nvidia/$APP_NAME:$NGC_VERSION diff --git a/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml b/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml index 3206248935..218a45c7a5 100644 --- a/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml +++ b/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml @@ -28,4 +28,4 @@ apiVersion: v1 appVersion: "2.59" description: Triton Inference Server name: triton-inference-server -version: 2.59.0 +version: 2.59.1 diff --git a/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml b/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml index 52b1ab2f21..94e5132d4d 100644 --- a/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml +++ b/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml @@ -31,14 +31,14 @@ maxReplicaCount: 3 tritonProtocol: HTTP # HPA GPU utilization autoscaling target HPATargetAverageValue: 85 -modelRepositoryPath: gs://triton_sample_models/25.06 -publishedVersion: '2.59.0' +modelRepositoryPath: gs://triton_sample_models/25.07 +publishedVersion: '2.59.1' gcpMarketplace: true image: registry: gcr.io repository: nvidia-ngc-public/tritonserver - tag: 25.06-py3 + tag: 25.07-py3 pullPolicy: IfNotPresent # modify the model repository here to match your GCP storage bucket numGpus: 1 diff --git a/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml b/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml index 36ba6b6a63..edfcbe164b 100644 --- a/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml +++ b/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml @@ -27,7 +27,7 @@ x-google-marketplace: schemaVersion: v2 applicationApiVersion: v1beta1 - publishedVersion: '2.59.0' + publishedVersion: '2.59.1' publishedVersionMetadata: releaseNote: >- Initial release. diff --git a/deploy/gke-marketplace-app/server-deployer/schema.yaml b/deploy/gke-marketplace-app/server-deployer/schema.yaml index 629303952d..ab21637bf3 100644 --- a/deploy/gke-marketplace-app/server-deployer/schema.yaml +++ b/deploy/gke-marketplace-app/server-deployer/schema.yaml @@ -27,7 +27,7 @@ x-google-marketplace: schemaVersion: v2 applicationApiVersion: v1beta1 - publishedVersion: '2.59.0' + publishedVersion: '2.59.1' publishedVersionMetadata: releaseNote: >- Initial release. @@ -89,7 +89,7 @@ properties: modelRepositoryPath: type: string title: Bucket where models are stored. Please make sure the user/service account to create the GKE app has permission to this GCS bucket. Read Triton documentation on configs and formatting details, supporting TensorRT, TensorFlow, Pytorch, Onnx ... etc. - default: gs://triton_sample_models/25.06 + default: gs://triton_sample_models/25.07 image.ldPreloadPath: type: string title: Leave this empty by default. Triton allows users to create custom layers for backend such as TensorRT plugin or Tensorflow custom ops, the compiled shared library must be provided via LD_PRELOAD environment variable. diff --git a/deploy/gke-marketplace-app/trt-engine/README.md b/deploy/gke-marketplace-app/trt-engine/README.md index dba63cf63e..c80da18f89 100644 --- a/deploy/gke-marketplace-app/trt-engine/README.md +++ b/deploy/gke-marketplace-app/trt-engine/README.md @@ -33,7 +33,7 @@ ``` docker run --gpus all -it --network host \ --shm-size=1g --ulimit memlock=-1 --ulimit stack=67108864 \ - -v ~:/scripts nvcr.io/nvidia/tensorrt:25.06-py3 + -v ~:/scripts nvcr.io/nvidia/tensorrt:25.07-py3 pip install onnx six torch tf2onnx tensorflow @@ -57,7 +57,7 @@ mkdir -p engines python3 builder.py -m models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/model.ckpt -o engines/bert_large_int8_bs1_s128.engine -b 1 -s 128 -c models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/ -v models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/vocab.txt --int8 --fp16 --strict --calib-num 1 -iln -imh -gsutil cp bert_large_int8_bs1_s128.engine gs://triton_sample_models/25.06/bert/1/model.plan +gsutil cp bert_large_int8_bs1_s128.engine gs://triton_sample_models/25.07/bert/1/model.plan ``` -For each Triton upgrade, container version used to generate the model, and the model path in GCS `gs://triton_sample_models/25.06/` should be updated accordingly with the correct version. +For each Triton upgrade, container version used to generate the model, and the model path in GCS `gs://triton_sample_models/25.07/` should be updated accordingly with the correct version. diff --git a/deploy/k8s-onprem/values.yaml b/deploy/k8s-onprem/values.yaml index ac6d7a3480..84d6c62f28 100644 --- a/deploy/k8s-onprem/values.yaml +++ b/deploy/k8s-onprem/values.yaml @@ -30,7 +30,7 @@ tags: openshift: false image: - imageName: nvcr.io/nvidia/tritonserver:25.06-py3 + imageName: nvcr.io/nvidia/tritonserver:25.07-py3 pullPolicy: IfNotPresent modelRepositoryServer: < Replace with the IP Address of your file server > modelRepositoryPath: /srv/models diff --git a/deploy/oci/values.yaml b/deploy/oci/values.yaml index be6eb13a0e..093791e1be 100644 --- a/deploy/oci/values.yaml +++ b/deploy/oci/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.06-py3 + imageName: nvcr.io/nvidia/tritonserver:25.07-py3 pullPolicy: IfNotPresent modelRepositoryPath: s3://https://.compat.objectstorage..oraclecloud.com:443/triton-inference-server-repository numGpus: 1 diff --git a/docs/introduction/compatibility.md b/docs/introduction/compatibility.md index 4332d4c8ae..450944756d 100644 --- a/docs/introduction/compatibility.md +++ b/docs/introduction/compatibility.md @@ -38,6 +38,7 @@ | Triton release version | NGC Tag | Python version | Torch version | TensorRT version | TensorRT-LLM version | CUDA version | CUDA Driver version | Size | | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 25.07 | nvcr.io/nvidia/tritonserver:25.07-trtllm-python-py3 | Python 3.12.3 | 2.7.0a0+79aa17489c.nv25.4 | 10.10.0.31 | 0.20.0 | 12.9.0.036 | 575.51.03 | 18.3G | | 25.06 | nvcr.io/nvidia/tritonserver:25.06-trtllm-python-py3 | Python 3.12.3 | 2.7.0a0+79aa17489c.nv25.4 | 10.10.0.31 | 0.20.0 | 12.9.0.036 | 575.51.03 | 18.3G | | 25.05 | nvcr.io/nvidia/tritonserver:25.05-trtllm-python-py3 | Python 3.12.3 | 2.7.0a0+7c8ec84dab.nv25.3 | 10.9.0.34 | 0.19.0 | 12.8.1.012 | 570.124.06 | 17G | | 25.04 | nvcr.io/nvidia/tritonserver:25.04-trtllm-python-py3 | Python 3.12.3 | 2.7.0a0+7c8ec84dab.nv25.3 | 10.9.0.34 | 0.18.2 | 12.8.1.012 | 570.124.06 | 17G | diff --git a/python/openai/README.md b/python/openai/README.md index 7d77620a55..d667dd1501 100644 --- a/python/openai/README.md +++ b/python/openai/README.md @@ -51,7 +51,7 @@ docker run -it --net=host --gpus all --rm \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ -e HF_TOKEN \ - nvcr.io/nvidia/tritonserver:25.06-vllm-python-py3 + nvcr.io/nvidia/tritonserver:25.07-vllm-python-py3 ``` 2. Launch the OpenAI-compatible Triton Inference Server: diff --git a/qa/common/gen_jetson_trt_models b/qa/common/gen_jetson_trt_models index b081fe9c4e..5bbfb4c74f 100755 --- a/qa/common/gen_jetson_trt_models +++ b/qa/common/gen_jetson_trt_models @@ -34,7 +34,7 @@ # Make all generated files accessible outside of container umask 0000 # Set the version of the models -TRITON_VERSION=${TRITON_VERSION:=25.06} +TRITON_VERSION=${TRITON_VERSION:=25.07} # Set the CUDA device to use CUDA_DEVICE=${RUNNER_ID:=0} # Set TensorRT image diff --git a/qa/common/gen_qa_custom_ops b/qa/common/gen_qa_custom_ops index 108b1daa53..6b3f349f44 100755 --- a/qa/common/gen_qa_custom_ops +++ b/qa/common/gen_qa_custom_ops @@ -37,7 +37,7 @@ ## ############################################################################ -TRITON_VERSION=${TRITON_VERSION:=25.06} +TRITON_VERSION=${TRITON_VERSION:=25.07} NVIDIA_UPSTREAM_VERSION=${NVIDIA_UPSTREAM_VERSION:=$TRITON_VERSION} PYTORCH_IMAGE=${PYTORCH_IMAGE:=nvcr.io/nvidia/pytorch:$NVIDIA_UPSTREAM_VERSION-py3} UBUNTU_IMAGE=${UBUNTU_IMAGE:=ubuntu:24.04} diff --git a/qa/common/gen_qa_model_repository b/qa/common/gen_qa_model_repository index d7f51c8f02..f84c0603d0 100755 --- a/qa/common/gen_qa_model_repository +++ b/qa/common/gen_qa_model_repository @@ -48,7 +48,7 @@ ## ############################################################################ -TRITON_VERSION=${TRITON_VERSION:=25.06} +TRITON_VERSION=${TRITON_VERSION:=25.07} # ONNX. Use ONNX_OPSET 0 to use the default for ONNX version ONNX_VERSION=1.16.1 From 5a9799191586f7925a4b8bde9bb28000fd926801 Mon Sep 17 00:00:00 2001 From: Sai Kiran Polisetty Date: Thu, 7 Aug 2025 16:26:24 +0530 Subject: [PATCH 062/230] ci: Improve error handling for Python backend model initialization failures (#8303) --- qa/L0_backend_python/test.sh | 59 ++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/qa/L0_backend_python/test.sh b/qa/L0_backend_python/test.sh index b2e6b8f034..58b92590e1 100755 --- a/qa/L0_backend_python/test.sh +++ b/qa/L0_backend_python/test.sh @@ -407,6 +407,65 @@ and shared memory pages after starting triton equals to $current_num_pages \n*** exit 1 fi + +# Test model with non-existent model file +# The model.py file is intentionally not created to trigger the failure. +rm -fr ./models +mkdir -p models/non_existent_model/1 +cp ../python_models/identity_fp32/config.pbtxt ./models/non_existent_model/config.pbtxt +(cd models/non_existent_model && \ + sed -i "s/^name:.*/name: \"non_existent_model\"/" config.pbtxt) + +ERROR_MESSAGE_1="Failed to preinitialize Python stub: Python model file not found in '`pwd`/models/non_existent_model/1/model.py'" +ERROR_MESSAGE_2="failed to load 'non_existent_model'" + +for test_mode in "default" "auto_config_disabled"; do + if [ "$test_mode" == "default" ]; then + SERVER_LOG_SUFFIX="" + SERVER_ARGS=$BASE_SERVER_ARGS + else + SERVER_LOG_SUFFIX="_auto_config_disabled" + SERVER_ARGS="$BASE_SERVER_ARGS --disable-auto-complete-config" + fi + + SERVER_LOG="./non_existent_model_server${SERVER_LOG_SUFFIX}.log" + CLIENT_LOG="./non_existent_model_client${SERVER_LOG_SUFFIX}.log" + + prev_num_pages=`get_shm_pages` + run_server + if [ "$SERVER_PID" != "0" ]; then + echo -e "*** FAILED: unexpected success starting $SERVER" >> $CLIENT_LOG + RET=1 + kill_server + else + if grep -q "$ERROR_MESSAGE_1" $SERVER_LOG; then + echo -e "Found \"$ERROR_MESSAGE_1\"" >> $CLIENT_LOG + else + echo -e "Not found \"$ERROR_MESSAGE_1\" in $SERVER_LOG" >> $CLIENT_LOG + cat $SERVER_LOG >> $CLIENT_LOG + RET=1 + fi + + if grep -q "$ERROR_MESSAGE_2" $SERVER_LOG; then + echo -e "Found \"$ERROR_MESSAGE_2\"" >> $CLIENT_LOG + else + echo -e "Not found \"$ERROR_MESSAGE_2\" in $SERVER_LOG" >> $CLIENT_LOG + cat $SERVER_LOG >> $CLIENT_LOG + RET=1 + fi + fi + + current_num_pages=`get_shm_pages` + if [ $current_num_pages -ne $prev_num_pages ]; then + cat $SERVER_LOG + ls /dev/shm + echo -e "\n***\n*** Test Failed. Shared memory pages were not cleaned properly. +Shared memory pages before starting triton equals to $prev_num_pages +and shared memory pages after starting triton equals to $current_num_pages \n***" + exit 1 + fi +done + # Disable env test for Jetson since cloud storage repos are not supported # Disable ensemble, io and bls tests for Jetson since GPU Tensors are not supported # Disable variants test for Jetson since already built without GPU Tensor support From a4d189743cd98146d7b5723f3eb842192a693773 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Thu, 7 Aug 2025 04:43:32 -0700 Subject: [PATCH 063/230] fix: Proper handling of system shm register offset (#8330) --- qa/L0_shared_memory/shared_memory_test.py | 48 ++++++++++++++-- qa/L0_shared_memory/test.sh | 68 +++++++++++++++++++++++ qa/common/infer_util.py | 39 ++++++++++--- src/shared_memory_manager.cc | 7 +-- 4 files changed, 143 insertions(+), 19 deletions(-) diff --git a/qa/L0_shared_memory/shared_memory_test.py b/qa/L0_shared_memory/shared_memory_test.py index 35667aacfa..63f3700c05 100755 --- a/qa/L0_shared_memory/shared_memory_test.py +++ b/qa/L0_shared_memory/shared_memory_test.py @@ -113,10 +113,6 @@ def _configure_server( shm_op1_handle, ] # Implicit assumption that input and output byte_sizes are 64 bytes for now - input0_data = np.arange(start=0, stop=16, dtype=np.int32) - input1_data = np.ones(shape=16, dtype=np.int32) - shm.set_shared_memory_region(shm_ip0_handle, [input0_data]) - shm.set_shared_memory_region(shm_ip1_handle, [input1_data]) self.triton_client.register_system_shared_memory( "input0_data", "/input0_data", register_byte_size, offset=register_offset ) @@ -129,6 +125,16 @@ def _configure_server( self.triton_client.register_system_shared_memory( "output1_data", "/output1_data", register_byte_size, offset=register_offset ) + + # Write data to shared memory regions + input0_data = np.arange(start=0, stop=16, dtype=np.int32) + input1_data = np.ones(shape=16, dtype=np.int32) + shm.set_shared_memory_region( + shm_ip0_handle, [input0_data], offset=register_offset + ) + shm.set_shared_memory_region( + shm_ip1_handle, [input1_data], offset=register_offset + ) self.shm_names = ["input0_data", "input1_data", "output0_data", "output1_data"] def _cleanup_shm_handles(self): @@ -292,6 +298,40 @@ def test_too_big_shm(self): self._shm_handles.append(shm_ip2_handle) self._cleanup_shm_handles() + def test_large_shm_register_offset(self): + # Test for out of bounds read vulnerability when registering system shared memory with large offset + for platform in ["python", "onnx", "libtorch", "plan", "openvino"]: + model_name = f"{platform}_int32_int32_int32" + + # Test for large offset + error_msg = [] + page_size = os.sysconf("SC_PAGE_SIZE") + # Create a large shm size (page_size * 1024 is large enough to reproduce a segfault). + # Register offset at 1 page before the end of the shm region to give enough space for the input/output data. + create_byte_size = page_size * 1024 + register_offset = page_size * 1023 + self._configure_server( + create_byte_size=create_byte_size, + register_offset=register_offset, + ) + + iu.shm_basic_infer( + self, + self.triton_client, + self._shm_handles[0], + self._shm_handles[1], + self._shm_handles[2], + self._shm_handles[3], + error_msg, + register_offset=register_offset, + protocol=self.protocol, + use_system_shared_memory=True, + override_model_name=model_name, + ) + self.triton_client.unregister_system_shared_memory() + if len(error_msg) > 0: + raise Exception(str(error_msg)) + def test_mixed_raw_shm(self): # Mix of shared memory and RAW inputs error_msg = [] diff --git a/qa/L0_shared_memory/test.sh b/qa/L0_shared_memory/test.sh index 55cdb80951..d34ce1a4e5 100755 --- a/qa/L0_shared_memory/test.sh +++ b/qa/L0_shared_memory/test.sh @@ -25,12 +25,26 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +REPO_VERSION=${NVIDIA_TRITON_SERVER_VERSION} +if [ "$#" -ge 1 ]; then + REPO_VERSION=$1 +fi +if [ -z "$REPO_VERSION" ]; then + echo -e "Repository version must be specified" + echo -e "\n***\n*** Test Failed\n***" + exit 1 +fi +if [ ! -z "$TEST_REPO_ARCH" ]; then + REPO_VERSION=${REPO_VERSION}_${TEST_REPO_ARCH} +fi + CLIENT_LOG="./client.log" SHM_TEST=shared_memory_test.py TEST_RESULT_FILE='test_results.txt' # Configure to support test on jetson as well TRITON_DIR=${TRITON_DIR:="/opt/tritonserver"} +DATADIR=/data/inferenceserver/${REPO_VERSION} SERVER=${TRITON_DIR}/bin/tritonserver BACKEND_DIR=${TRITON_DIR}/backends SERVER_ARGS_EXTRA="--backend-directory=${BACKEND_DIR}" @@ -142,6 +156,60 @@ for test_case in \ done done +# Test large system shared memory offset +rm -rf models/* +# prepare add_sub model of various backends +BACKENDS="python onnx libtorch plan openvino" +for backend in ${BACKENDS} ; do + model="${backend}_int32_int32_int32" + model_dir="models/${model}" + if [[ $backend == "python" ]]; then + mkdir -p ${model_dir}/1 + cp ../python_models/add_sub/model.py ${model_dir}/1/ + cp ../python_models/add_sub/config.pbtxt ${model_dir}/ + sed -i 's/TYPE_FP32/TYPE_INT32/g' ${model_dir}/config.pbtxt + echo "max_batch_size: 8" >> ${model_dir}/config.pbtxt + else + mkdir -p ${model_dir} + cp -r $DATADIR/qa_model_repository/${model}/1 ${model_dir}/1 + cp $DATADIR/qa_model_repository/${model}/config.pbtxt ${model_dir}/ + cp $DATADIR/qa_model_repository/${model}/output0_labels.txt ${model_dir}/ + if [ $backend == "openvino" ]; then + echo 'parameters { key: "ENABLE_BATCH_PADDING" value { string_value: "YES" } }' >> models/${model}/config.pbtxt + fi + fi +done + +test_case="test_large_shm_register_offset" +for client_type in http grpc; do + SERVER_ARGS="--model-repository=`pwd`/models --log-verbose=1 ${SERVER_ARGS_EXTRA}" + SERVER_LOG="./${test_case}.${client_type}.server.log" + run_server + if [ "$SERVER_PID" == "0" ]; then + echo -e "\n***\n*** Failed to start $SERVER\n***" + cat $SERVER_LOG + exit 1 + fi + + export CLIENT_TYPE=$client_type + CLIENT_LOG="./${test_case}.${client_type}.client.log" + set +e + python3 $SHM_TEST SharedMemoryTest.${test_case} >>"$CLIENT_LOG" 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed - ${client_type}\n***" + RET=1 + fi + + kill $SERVER_PID + wait $SERVER_PID + if [ $? -ne 0 ]; then + echo -e "\n***\n*** Test Server shut down non-gracefully\n***" + RET=1 + fi + set -e +done + if [ $RET -eq 0 ]; then echo -e "\n***\n*** Test Passed\n***" else diff --git a/qa/common/infer_util.py b/qa/common/infer_util.py index edaf0ede47..7e9c775570 100755 --- a/qa/common/infer_util.py +++ b/qa/common/infer_util.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2018-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -1367,11 +1367,13 @@ def shm_basic_infer( big_shm_name="", big_shm_size=64, default_shm_byte_size=64, + register_offset=0, shm_output_offset=0, shm_output_byte_size=64, protocol="http", use_system_shared_memory=False, use_cuda_shared_memory=False, + override_model_name=None, ): # Lazy shm imports... if use_system_shared_memory: @@ -1381,6 +1383,16 @@ def shm_basic_infer( else: raise Exception("No shared memory type specified") + if override_model_name is None: + model_name = "simple" + else: + model_name = override_model_name + + if model_name.startswith("libtorch"): + output_names = ["OUTPUT__0", "OUTPUT__1"] + else: + output_names = ["OUTPUT0", "OUTPUT1"] + input0_data = np.arange(start=0, stop=16, dtype=np.int32) input1_data = np.ones(shape=16, dtype=np.int32) inputs = [] @@ -1388,13 +1400,17 @@ def shm_basic_infer( if protocol == "http": inputs.append(httpclient.InferInput("INPUT0", [1, 16], "INT32")) inputs.append(httpclient.InferInput("INPUT1", [1, 16], "INT32")) - outputs.append(httpclient.InferRequestedOutput("OUTPUT0", binary_data=True)) - outputs.append(httpclient.InferRequestedOutput("OUTPUT1", binary_data=False)) + outputs.append( + httpclient.InferRequestedOutput(output_names[0], binary_data=True) + ) + outputs.append( + httpclient.InferRequestedOutput(output_names[1], binary_data=False) + ) else: inputs.append(grpcclient.InferInput("INPUT0", [1, 16], "INT32")) inputs.append(grpcclient.InferInput("INPUT1", [1, 16], "INT32")) - outputs.append(grpcclient.InferRequestedOutput("OUTPUT0")) - outputs.append(grpcclient.InferRequestedOutput("OUTPUT1")) + outputs.append(grpcclient.InferRequestedOutput(output_names[0])) + outputs.append(grpcclient.InferRequestedOutput(output_names[1])) inputs[0].set_shared_memory("input0_data", default_shm_byte_size) @@ -1414,9 +1430,9 @@ def shm_basic_infer( try: results = triton_client.infer( - "simple", inputs, model_version="", outputs=outputs + model_name, inputs, model_version="", outputs=outputs ) - output = results.get_output("OUTPUT0") + output = results.get_output(output_names[0]) if protocol == "http": output_datatype = output["datatype"] output_shape = output["shape"] @@ -1427,11 +1443,16 @@ def shm_basic_infer( if use_system_shared_memory: output_data = shm.get_contents_as_numpy( - shm_op0_handle, output_dtype, output_shape + shm_op0_handle, + output_dtype, + output_shape, + offset=register_offset + shm_output_offset, ) elif use_cuda_shared_memory: output_data = cudashm.get_contents_as_numpy( - shm_op0_handle, output_dtype, output_shape + shm_op0_handle, + output_dtype, + output_shape, ) tester.assertTrue( diff --git a/src/shared_memory_manager.cc b/src/shared_memory_manager.cc index 80b739ee1b..a2d52f5f48 100644 --- a/src/shared_memory_manager.cc +++ b/src/shared_memory_manager.cc @@ -524,12 +524,7 @@ SharedMemoryManager::GetMemoryInfo( *shm_info = std::static_pointer_cast(it->second); } - if (it->second->kind_ == TRITONSERVER_MEMORY_CPU) { - *shm_mapped_addr = (void*)((uint8_t*)it->second->mapped_addr_ + - it->second->offset_ + offset); - } else { - *shm_mapped_addr = (void*)((uint8_t*)it->second->mapped_addr_ + offset); - } + *shm_mapped_addr = (void*)((uint8_t*)it->second->mapped_addr_ + offset); *memory_type = it->second->kind_; *device_id = it->second->device_id_; From 50ff906d5a2fb833b96970b91069ef32ea3eef3a Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Fri, 8 Aug 2025 17:30:49 -0700 Subject: [PATCH 064/230] fix: System shared memory boundary check (#8335) --- qa/L0_shared_memory/shared_memory_test.py | 22 ++++++++++++++++------ src/shared_memory_manager.cc | 13 +++++-------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/qa/L0_shared_memory/shared_memory_test.py b/qa/L0_shared_memory/shared_memory_test.py index 63f3700c05..f40531b2e6 100755 --- a/qa/L0_shared_memory/shared_memory_test.py +++ b/qa/L0_shared_memory/shared_memory_test.py @@ -47,6 +47,7 @@ class SystemSharedMemoryTestBase(tu.TestResultCollector): DEFAULT_SHM_BYTE_SIZE = 64 + SYS_PAGE_SIZE = os.sysconf("SC_PAGE_SIZE") def setUp(self): self._setup_client() @@ -305,11 +306,10 @@ def test_large_shm_register_offset(self): # Test for large offset error_msg = [] - page_size = os.sysconf("SC_PAGE_SIZE") # Create a large shm size (page_size * 1024 is large enough to reproduce a segfault). # Register offset at 1 page before the end of the shm region to give enough space for the input/output data. - create_byte_size = page_size * 1024 - register_offset = page_size * 1023 + create_byte_size = self.SYS_PAGE_SIZE * 1024 + register_offset = self.SYS_PAGE_SIZE * 1023 self._configure_server( create_byte_size=create_byte_size, register_offset=register_offset, @@ -372,7 +372,12 @@ def test_unregisterall(self): def test_infer_offset_out_of_bound(self): # Shared memory offset outside output region - Throws error error_msg = [] - self._configure_server() + create_byte_size = self.SYS_PAGE_SIZE + self.DEFAULT_SHM_BYTE_SIZE + register_offset = self.SYS_PAGE_SIZE + self._configure_server( + create_byte_size=create_byte_size, + register_offset=register_offset, + ) if self.protocol == "http": # -32 when placed in an int64 signed type, to get a negative offset # by overflowing @@ -402,8 +407,13 @@ def test_infer_offset_out_of_bound(self): def test_infer_byte_size_out_of_bound(self): # Shared memory byte_size outside output region - Throws error error_msg = [] - self._configure_server() - offset = 60 + create_byte_size = self.SYS_PAGE_SIZE + self.DEFAULT_SHM_BYTE_SIZE + register_offset = self.SYS_PAGE_SIZE + self._configure_server( + create_byte_size=create_byte_size, + register_offset=register_offset, + ) + offset = 1 byte_size = self.DEFAULT_SHM_BYTE_SIZE iu.shm_basic_infer( diff --git a/src/shared_memory_manager.cc b/src/shared_memory_manager.cc index a2d52f5f48..bde10b525c 100644 --- a/src/shared_memory_manager.cc +++ b/src/shared_memory_manager.cc @@ -483,14 +483,11 @@ SharedMemoryManager::GetMemoryInfo( } // validate offset - size_t shm_region_end = 0; - if (it->second->kind_ == TRITONSERVER_MEMORY_CPU) { - shm_region_end = it->second->offset_; - } + size_t shm_region_size = 0; if (it->second->byte_size_ > 0) { - shm_region_end += it->second->byte_size_ - 1; + shm_region_size += it->second->byte_size_; } - if (offset > shm_region_end) { + if (offset >= shm_region_size) { return TRITONSERVER_ErrorNew( TRITONSERVER_ERROR_INVALID_ARG, std::string("Invalid offset for shared memory region: '" + name + "'") @@ -510,8 +507,8 @@ SharedMemoryManager::GetMemoryInfo( } // validate byte_size + offset is within memory bounds - size_t total_req_shm = offset + byte_size - 1; - if (total_req_shm > shm_region_end) { + size_t total_req_shm = offset + byte_size; + if (total_req_shm > shm_region_size) { return TRITONSERVER_ErrorNew( TRITONSERVER_ERROR_INVALID_ARG, std::string( From 21245cf59e4b1670c6aea6c077d8c45b7de254e4 Mon Sep 17 00:00:00 2001 From: Sai Kiran Polisetty Date: Mon, 11 Aug 2025 10:47:35 +0530 Subject: [PATCH 065/230] fix: Improve validation for system shared memory register (#8336) --- qa/L0_shared_memory/shared_memory_test.py | 43 +++++++++++++++++++---- qa/L0_shared_memory/test.sh | 1 + src/common.cc | 35 ++++++++++++++++++ src/common.h | 10 ++++++ src/shared_memory_manager.cc | 10 +----- 5 files changed, 83 insertions(+), 16 deletions(-) diff --git a/qa/L0_shared_memory/shared_memory_test.py b/qa/L0_shared_memory/shared_memory_test.py index f40531b2e6..6f8ba94337 100755 --- a/qa/L0_shared_memory/shared_memory_test.py +++ b/qa/L0_shared_memory/shared_memory_test.py @@ -566,16 +566,45 @@ def test_register_reserved_names(self): """ # This matches kTritonSharedMemoryRegionPrefix in the server code. reserved_prefix = "triton_python_backend_shm_region_" + shm_name = "my_test_shm_name" + + # The shared memory key cannot start with the reserved prefix, + # regardless of leading slashes. + shm_keys_to_test = [ + f"{reserved_prefix}_my_test_shm_key", + f"/{reserved_prefix}_my_test_shm_key", + f"///{reserved_prefix}_my_test_shm_key", + ] - # The shared memory key cannot start with the reserved prefix. + for shm_key in shm_keys_to_test: + with self.subTest(shm_key=shm_key): + expected_msg = f"cannot register shared memory region '{shm_name}' with key '{shm_key}' as the key contains the reserved prefix '{reserved_prefix}'" + with self.assertRaisesRegex( + utils.InferenceServerException, expected_msg + ): + self.triton_client.register_system_shared_memory( + shm_name, shm_key, 10000 + ) + + def test_register_invalid_shm_key(self): + """ + Test that registration fails if attempting to use an invalid name for the shm key. + """ shm_name = "my_test_shm_name" - shm_key = f"{reserved_prefix}_my_test_shm_key" + shm_keys_to_test = [ + "/", + "///", + ] - with self.assertRaisesRegex( - utils.InferenceServerException, - f"cannot register shared memory region '{shm_name}' with key '{shm_key}' as the key contains the reserved prefix '{reserved_prefix}'", - ) as e: - self.triton_client.register_system_shared_memory(shm_name, shm_key, 10000) + for shm_key in shm_keys_to_test: + with self.subTest(shm_key=shm_key): + expected_msg = f"cannot register shared memory region '{shm_name}' - invalid shm key '{shm_key}'" + with self.assertRaisesRegex( + utils.InferenceServerException, expected_msg + ): + self.triton_client.register_system_shared_memory( + shm_name, shm_key, 10000 + ) def callback(user_data, result, error): diff --git a/qa/L0_shared_memory/test.sh b/qa/L0_shared_memory/test.sh index d34ce1a4e5..3f5758bf36 100755 --- a/qa/L0_shared_memory/test.sh +++ b/qa/L0_shared_memory/test.sh @@ -70,6 +70,7 @@ for i in \ test_infer_integer_overflow \ test_register_out_of_bound \ test_register_reserved_names \ + test_register_invalid_shm_key \ test_python_client_leak; do for client_type in http grpc; do SERVER_ARGS="--model-repository=`pwd`/models --log-verbose=1 ${SERVER_ARGS_EXTRA}" diff --git a/src/common.cc b/src/common.cc index 90725041d1..a7591b8324 100644 --- a/src/common.cc +++ b/src/common.cc @@ -163,4 +163,39 @@ DecodeBase64( return nullptr; } +TRITONSERVER_Error* +ValidateSharedMemoryKey(const std::string& name, const std::string& shm_key) +{ + std::string_view key_view(shm_key); + + // Find the index of the first character that is not a slash + const std::size_t first_non_slash = key_view.find_first_not_of('/'); + + // If the entire key is slashes + if (first_non_slash == std::string_view::npos) { + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + std::string( + "cannot register shared memory region '" + name + + "' - invalid shm key '" + shm_key + "'") + .c_str()); + } + + // Check whether the substring starting at first_non_slash starts with the + // reserved prefix + if (key_view.substr(first_non_slash) + .rfind(kTritonSharedMemoryRegionPrefix, 0) == 0) { + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + std::string( + "cannot register shared memory region '" + name + "' with key '" + + shm_key + "' as the key contains the reserved prefix '" + + kTritonSharedMemoryRegionPrefix + "'") + .c_str()); + } + + // Valid shm key + return nullptr; +} + }} // namespace triton::server diff --git a/src/common.h b/src/common.h index 7df618e965..403aec9a6a 100644 --- a/src/common.h +++ b/src/common.h @@ -196,6 +196,16 @@ TRITONSERVER_Error* DecodeBase64( const char* input, size_t input_len, std::vector& decoded_data, size_t& decoded_size, const std::string& name); + +/// Validate shared memory key +/// +/// \param name The name of the memory block. +/// \param shm_key The name of the posix shared memory object +/// \return The error status. +TRITONSERVER_Error* ValidateSharedMemoryKey( + const std::string& name, const std::string& shm_key); + + /// Joins container of strings into a single string delimited by /// 'delim'. /// diff --git a/src/shared_memory_manager.cc b/src/shared_memory_manager.cc index bde10b525c..058bd73c1d 100644 --- a/src/shared_memory_manager.cc +++ b/src/shared_memory_manager.cc @@ -355,15 +355,7 @@ SharedMemoryManager::RegisterSystemSharedMemory( const size_t byte_size) { // Check if the shared memory key starts with the reserved prefix - if (shm_key.rfind(kTritonSharedMemoryRegionPrefix, 0) == 0) { - return TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INVALID_ARG, - std::string( - "cannot register shared memory region '" + name + "' with key '" + - shm_key + "' as the key contains the reserved prefix '" + - kTritonSharedMemoryRegionPrefix + "'") - .c_str()); - } + RETURN_IF_ERR(ValidateSharedMemoryKey(name, shm_key)); std::lock_guard lock(mu_); From 5e3eec9452c58691b6a05914e36eacdb7a88b73f Mon Sep 17 00:00:00 2001 From: mattwittwer Date: Wed, 13 Aug 2025 09:10:42 -0700 Subject: [PATCH 066/230] fix: Add testing for explicit model load (#8276) Add tests to confirm python_backend model load parameters are correctly validated --- .../model_control/model_control_test.py | 241 +++++++++++++++++- qa/L0_backend_python/model_control/test.sh | 15 +- 2 files changed, 254 insertions(+), 2 deletions(-) diff --git a/qa/L0_backend_python/model_control/model_control_test.py b/qa/L0_backend_python/model_control/model_control_test.py index 9ccb73df4f..a0ad197bb8 100755 --- a/qa/L0_backend_python/model_control/model_control_test.py +++ b/qa/L0_backend_python/model_control/model_control_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2021-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -26,7 +26,10 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import base64 +import json import os +import subprocess import sys sys.path.append("../../common") @@ -83,5 +86,241 @@ def test_model_reload(self): self.assertFalse(client.is_model_ready(ensemble_model_name)) +class ModelIDValidationTest(unittest.TestCase): + """ + Test model ID validation for user-provided model names. + + Verifies that model names containing dangerous characters are properly rejected. + Uses raw HTTP requests via curl instead of the Triton client to test server-side + validation without the Triton client encoding special characters. + """ + + def setUp(self): + self._shm_leak_detector = shm_util.ShmLeakDetector() + self._client = httpclient.InferenceServerClient(f"{_tritonserver_ipaddr}:8000") + self._triton_host = _tritonserver_ipaddr + self._triton_port = 8000 + + # Check if curl is available + try: + subprocess.run(["curl", "--version"], capture_output=True, check=True) + except (subprocess.CalledProcessError, FileNotFoundError): + self.skipTest("curl command not available - required for raw HTTP testing") + + def _send_load_model_request(self, model_name): + """Send HTTP request to load model for testing input validation using curl""" + + # Create simple Triton Python model code + python_model_code = f"""import triton_python_backend_utils as pb_utils + +class TritonPythonModel: + def execute(self, requests): + print('Hello world from model {model_name}') + responses = [] + for request in requests: + # Simple identity function + input_tensor = pb_utils.get_input_tensor_by_name(request, "INPUT0") + out_tensor = pb_utils.Tensor("OUTPUT0", input_tensor.as_numpy()) + responses.append(pb_utils.InferenceResponse([out_tensor])) + return responses""" + + # Base64 encode the Python code (as required by Triton server) + python_code_b64 = base64.b64encode(python_model_code.encode("utf-8")).decode( + "ascii" + ) + + # Create simple config + config = { + "name": model_name, + "backend": "python", + "max_batch_size": 4, + "input": [{"name": "INPUT0", "data_type": "TYPE_FP32", "dims": [-1]}], + "output": [{"name": "OUTPUT0", "data_type": "TYPE_FP32", "dims": [-1]}], + } + + payload = { + "parameters": { + "config": json.dumps(config), + "file:/1/model.py": python_code_b64, + } + } + + url = f"http://{self._triton_host}:{self._triton_port}/v2/repository/models/{model_name}/load" + + # Convert payload to JSON string + payload_json = json.dumps(payload) + + try: + # Use curl to send the request + curl_cmd = [ + "curl", + "-s", + "-w", + "\n%{http_code}", # Write HTTP status code on separate line + "-X", + "POST", + "-H", + "Content-Type: application/json", + "-d", + payload_json, + url, + ] + + result = subprocess.run( + curl_cmd, capture_output=True, text=True, timeout=10 + ) + + # Parse curl output - last line is status code, rest is response body + output_lines = ( + result.stdout.strip().split("\n") if result.stdout.strip() else [] + ) + if len(output_lines) >= 2: + try: + status_code = int(output_lines[-1]) + response_text = "\n".join(output_lines[:-1]) + except ValueError: + status_code = 0 + response_text = result.stdout or result.stderr or "Invalid response" + elif len(output_lines) == 1 and output_lines[0].isdigit(): + status_code = int(output_lines[0]) + response_text = result.stderr or "No response body" + else: + status_code = 0 + response_text = result.stdout or result.stderr or "No response" + + # Return an object similar to requests.Response + class CurlResponse: + def __init__(self, status_code, text): + self.status_code = status_code + self.text = text + self.content = text.encode() + + return CurlResponse(status_code, response_text) + + except ( + subprocess.TimeoutExpired, + subprocess.CalledProcessError, + ValueError, + ) as e: + # Return a mock response for errors + class ErrorResponse: + def __init__(self, error_msg): + self.status_code = 0 + self.text = f"Error: {error_msg}" + self.content = self.text.encode() + + return ErrorResponse(str(e)) + + def test_invalid_character_model_names(self): + """Test that model names with invalid characters are properly rejected""" + + # Based on INVALID_CHARS = ";|&$`<>()[]{}\\\"'*?~#!" + invalid_model_names = [ + r"model;test", + r"model|test", + r"model&test", + r"model$test", + r"model`test`", + r"model", + r"model(test)", + # r"model[test]", # request fails to send unencoded + r"model{test}", + r"model\test", + r'model"test"', + r"model'test'", + r"model*test", + # r"model?test", # request fails to send unencoded + r"model~test", + # r"model#test", # request fails to send unencoded + r"model!test", + ] + + for invalid_name in invalid_model_names: + with self.subTest(model_name=invalid_name): + print(f"Testing invalid model name: {invalid_name}") + + response = self._send_load_model_request(invalid_name) + print( + f"Response for '{invalid_name}': Status {response.status_code}, Text: {response.text[:200]}..." + ) + + # Should not get a successful 200 response + self.assertNotEqual( + 200, + response.status_code, + f"Invalid model name '{invalid_name}' should not get 200 OK response", + ) + + # Special case for curly braces - they get stripped and cause load failures prior to the validation check + if "{" in invalid_name or "}" in invalid_name: + self.assertIn( + "failed to load", + response.text, + f"Model with curly braces '{invalid_name}' should fail to load", + ) + else: + # Normal case - should get character validation error + self.assertIn( + "Invalid stub name: contains invalid characters", + response.text, + f"invalid response for '{invalid_name}' should contain 'Invalid stub name: contains invalid characters'", + ) + + # Verify the model is not loaded/ready since it was rejected + try: + self.assertFalse( + self._client.is_model_ready(invalid_name), + f"Model '{invalid_name}' should not be ready after failed load attempt", + ) + except Exception as e: + # If checking model readiness fails, that's also acceptable since the model name is invalid + print( + f"Note: Could not check model readiness for '{invalid_name}': {e}" + ) + + def test_valid_model_names(self): + """Test that valid model names work""" + + valid_model_names = [ + "TestModel123", + "model-with-hyphens", + "model_with_underscores", + ] + + for valid_name in valid_model_names: + with self.subTest(model_name=valid_name): + print(f"Testing valid model name: {valid_name}") + + response = self._send_load_model_request(valid_name) + print( + f"Response for valid '{valid_name}': Status {response.status_code}, Text: {response.text[:100]}..." + ) + + # Valid model names should be accepted and load successfully + self.assertEqual( + 200, + response.status_code, + f"Valid model name '{valid_name}' should get 200 OK response, got {response.status_code}. Response: {response.text}", + ) + + # Should not contain validation error message + self.assertNotIn( + "Invalid stub name: contains invalid characters", + response.text, + f"Valid model name '{valid_name}' should not contain validation error message", + ) + + # Verify the model is actually loaded by checking if it's ready + try: + self.assertTrue( + self._client.is_model_ready(valid_name), + f"Model '{valid_name}' should be ready after successful load", + ) + # Clean up - unload the model after testing + self._client.unload_model(valid_name) + except Exception as e: + self.fail(f"Failed to check if model '{valid_name}' is ready: {e}") + + if __name__ == "__main__": unittest.main() diff --git a/qa/L0_backend_python/model_control/test.sh b/qa/L0_backend_python/model_control/test.sh index e2c22f2685..f841222a1b 100755 --- a/qa/L0_backend_python/model_control/test.sh +++ b/qa/L0_backend_python/model_control/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2021-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -55,11 +55,24 @@ if [ $? -ne 0 ]; then echo -e "\n***\n*** model_control_test.py FAILED. \n***" RET=1 fi + +echo -e "\n***\n*** Running model ID validation test\n***" +SUBTEST="model_id_validation" +python3 -m pytest --junitxml=model_control.${SUBTEST}.report.xml model_control_test.py::ModelIDValidationTest >> ${CLIENT_LOG} 2>&1 + +if [ $? -ne 0 ]; then + echo -e "\n***\n*** model_id_validation_test.py FAILED. \n***" + RET=1 +fi + set -e kill_server if [ $RET -eq 1 ]; then + echo -e "\n***\n*** Server logs:\n***" + cat $SERVER_LOG + echo -e "\n***\n*** Client logs:\n***" cat $CLIENT_LOG echo -e "\n***\n*** model_control_test FAILED. \n***" else From 7063d469afc77b13b6d13a26addbb03922ea936a Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Wed, 13 Aug 2025 12:31:21 -0700 Subject: [PATCH 067/230] Add CMake policy for legacy code (#8331) --- qa/L0_backend_bls/test.sh | 1 + qa/L0_backend_python/common.sh | 2 ++ qa/L0_backend_python/setup_python_enviroment.sh | 1 + qa/L0_backend_python/variants/test.sh | 3 ++- qa/L0_backend_tutorial/test.sh | 2 ++ qa/L0_batch_custom/test.sh | 2 ++ qa/L0_client_build_variants/test.sh | 6 ++++++ qa/L0_perf_kaldi/test.sh | 3 ++- 8 files changed, 18 insertions(+), 2 deletions(-) diff --git a/qa/L0_backend_bls/test.sh b/qa/L0_backend_bls/test.sh index 7dcdbd2028..9e27f50870 100755 --- a/qa/L0_backend_bls/test.sh +++ b/qa/L0_backend_bls/test.sh @@ -57,6 +57,7 @@ git clone --single-branch --depth=1 -b $TRITON_BACKEND_REPO_TAG \ (cd backend/examples/backends/bls && mkdir build && cd build && + export CMAKE_POLICY_VERSION_MINIMUM=3.5 && cmake -DCMAKE_INSTALL_PREFIX:PATH=`pwd`/install \ -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ -DTRITON_BACKEND_REPO_TAG=${TRITON_BACKEND_REPO_TAG} \ diff --git a/qa/L0_backend_python/common.sh b/qa/L0_backend_python/common.sh index 68bafb4420..e23fd6e95b 100755 --- a/qa/L0_backend_python/common.sh +++ b/qa/L0_backend_python/common.sh @@ -86,7 +86,9 @@ create_python_backend_stub() { rm -rf python_backend git clone ${TRITON_REPO_ORGANIZATION}/python_backend -b $PYTHON_BACKEND_REPO_TAG CUDA_PATH=$(readlink -f /usr/local/cuda) + export CMAKE_POLICY_VERSION_MINIMUM=3.5 (cd python_backend/ && mkdir builddir && cd builddir && \ + export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DTRITON_ENABLE_GPU=ON -DCMAKE_CUDA_COMPILER=$CUDA_PATH/bin/nvcc \ -DCUDAToolkit_ROOT=$CUDA_PATH -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ -DTRITON_BACKEND_REPO_TAG=$TRITON_BACKEND_REPO_TAG -DTRITON_COMMON_REPO_TAG=$TRITON_COMMON_REPO_TAG \ diff --git a/qa/L0_backend_python/setup_python_enviroment.sh b/qa/L0_backend_python/setup_python_enviroment.sh index 6e12f05dea..cfa56cba51 100755 --- a/qa/L0_backend_python/setup_python_enviroment.sh +++ b/qa/L0_backend_python/setup_python_enviroment.sh @@ -113,6 +113,7 @@ find /opt/tritonserver/qa/pkgs/ -maxdepth 1 -type f -name \ # Build triton-shm-monitor for the test cd python_backend && rm -rf install build && mkdir build && cd build && \ + export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DCMAKE_INSTALL_PREFIX:PATH=$PWD/install \ -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ -DTRITON_COMMON_REPO_TAG:STRING=${TRITON_COMMON_REPO_TAG} \ diff --git a/qa/L0_backend_python/variants/test.sh b/qa/L0_backend_python/variants/test.sh index 86cc793a94..3e3677cbe4 100755 --- a/qa/L0_backend_python/variants/test.sh +++ b/qa/L0_backend_python/variants/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2021, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -34,6 +34,7 @@ rm -rf python_backend git clone ${TRITON_REPO_ORGANIZATION}/python_backend -b $PYTHON_BACKEND_REPO_TAG (cd python_backend/ && mkdir builddir && cd builddir && \ + export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DTRITON_ENABLE_GPU=OFF -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} -DTRITON_BACKEND_REPO_TAG=$TRITON_BACKEND_REPO_TAG -DTRITON_COMMON_REPO_TAG=$TRITON_COMMON_REPO_TAG -DTRITON_CORE_REPO_TAG=$TRITON_CORE_REPO_TAG ../ && \ make -j18 install) diff --git a/qa/L0_backend_tutorial/test.sh b/qa/L0_backend_tutorial/test.sh index b58a5e2606..60c98c18f8 100755 --- a/qa/L0_backend_tutorial/test.sh +++ b/qa/L0_backend_tutorial/test.sh @@ -62,6 +62,7 @@ git clone --single-branch --depth=1 -b $TRITON_BACKEND_REPO_TAG \ (cd backend/examples/backends/minimal && mkdir build && cd build && + export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DCMAKE_INSTALL_PREFIX:PATH=`pwd`/install \ -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ -DTRITON_BACKEND_REPO_TAG=${TRITON_BACKEND_REPO_TAG} \ @@ -140,6 +141,7 @@ rm -fr /opt/tritonserver/backends/minimal (cd backend/examples/backends/recommended && mkdir build && cd build && + export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DCMAKE_INSTALL_PREFIX:PATH=`pwd`/install \ -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ -DTRITON_BACKEND_REPO_TAG=${TRITON_BACKEND_REPO_TAG} \ diff --git a/qa/L0_batch_custom/test.sh b/qa/L0_batch_custom/test.sh index c438862f5f..5ed0f3a91b 100755 --- a/qa/L0_batch_custom/test.sh +++ b/qa/L0_batch_custom/test.sh @@ -88,6 +88,7 @@ git clone --single-branch --depth=1 -b $TRITON_BACKEND_REPO_TAG \ (cd backend/examples/batching_strategies/volume_batching && mkdir build && cd build && + export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DCMAKE_INSTALL_PREFIX:PATH=`pwd`/install \ -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ -DTRITON_BACKEND_REPO_TAG=${TRITON_BACKEND_REPO_TAG} \ @@ -98,6 +99,7 @@ git clone --single-branch --depth=1 -b $TRITON_BACKEND_REPO_TAG \ (cd backend/examples/batching_strategies/single_batching && mkdir build && cd build && + export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DCMAKE_INSTALL_PREFIX:PATH=`pwd`/install \ -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ -DTRITON_BACKEND_REPO_TAG=${TRITON_BACKEND_REPO_TAG} \ diff --git a/qa/L0_client_build_variants/test.sh b/qa/L0_client_build_variants/test.sh index 91926e8a0f..31021b7b24 100755 --- a/qa/L0_client_build_variants/test.sh +++ b/qa/L0_client_build_variants/test.sh @@ -56,6 +56,7 @@ TRITON_COMMON_REPO_TAG=${TRITON_COMMON_REPO_TAG:="main"} (cd /workspace/build && \ rm -fr cc-clients java-clients python-clients && \ + export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ -DTRITON_ENABLE_CC_HTTP=ON \ -DTRITON_ENABLE_CC_GRPC=ON \ @@ -85,6 +86,7 @@ fi # (cd /workspace/build && \ rm -fr cc-clients python-clients && \ + export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ -DTRITON_ENABLE_CC_HTTP=OFF \ -DTRITON_ENABLE_CC_GRPC=ON \ @@ -112,6 +114,7 @@ fi # (cd /workspace/build && \ rm -fr cc-clients python-clients && \ + export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ -DTRITON_ENABLE_CC_HTTP=ON \ -DTRITON_ENABLE_CC_GRPC=OFF \ @@ -141,6 +144,7 @@ mkdir -p /workspace/build # Build without C API in Perf Analyzer # (cd /workspace/build && \ + export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ -DTRITON_ENABLE_CC_HTTP=ON \ -DTRITON_ENABLE_CC_GRPC=ON \ @@ -166,6 +170,7 @@ fi # (cd /workspace/build && \ rm -fr cc_clients perf_analyzer && \ + export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ -DTRITON_ENABLE_CC_HTTP=ON \ -DTRITON_ENABLE_CC_GRPC=ON \ @@ -191,6 +196,7 @@ fi # (cd /workspace/build && \ rm -fr cc_clients perf_analyzer && \ + export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ -DTRITON_ENABLE_CC_HTTP=ON \ -DTRITON_ENABLE_CC_GRPC=ON \ diff --git a/qa/L0_perf_kaldi/test.sh b/qa/L0_perf_kaldi/test.sh index 31d4c99ee6..79f5edf7e3 100755 --- a/qa/L0_perf_kaldi/test.sh +++ b/qa/L0_perf_kaldi/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2020-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -48,6 +48,7 @@ pip3 install --upgrade wheel setuptools grpcio-tools # Build client library and kaldi perf client (cd triton-inference-server/build && \ + export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX:PATH=/workspace/install && \ make -j16 trtis-clients) From d3817a18d8b89e4ff7a89b3651a73f9b7ae10bd3 Mon Sep 17 00:00:00 2001 From: Sai Kiran Polisetty Date: Fri, 15 Aug 2025 08:46:01 +0530 Subject: [PATCH 068/230] ci: Resolve CI failures (#8347) --- qa/L0_grpc/python_grpc_aio_test.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/qa/L0_grpc/python_grpc_aio_test.py b/qa/L0_grpc/python_grpc_aio_test.py index ba43b36abb..6466432c6b 100755 --- a/qa/L0_grpc/python_grpc_aio_test.py +++ b/qa/L0_grpc/python_grpc_aio_test.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -100,9 +100,11 @@ async def test_get_system_shared_memory_status(self): async def test_register_system_shared_memory(self): with self.assertRaisesRegex( InferenceServerException, - "\[StatusCode\.INTERNAL\] Unable to open shared memory region: ''", + "\[StatusCode\.INTERNAL\] Unable to open shared memory region: '/test_shm'", ): - await self._triton_client.register_system_shared_memory("", "", 0) + await self._triton_client.register_system_shared_memory( + "test_shm", "/test_shm", 0 + ) async def test_unregister_system_shared_memory(self): await self._triton_client.unregister_system_shared_memory() From b4cbfa2d04f07ddae6e4c6e46844a49bcebe1bc6 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Thu, 28 Aug 2025 11:55:27 -0700 Subject: [PATCH 069/230] post-25.08: Update default branch (#8366) Co-authored-by: Kris Hung Co-authored-by: Jacky <18255193+kthui@users.noreply.github.com> Co-authored-by: GuanLuo --- CONTRIBUTING.md | 2 +- Dockerfile.sdk | 4 +- README.md | 13 +- TRITON_VERSION | 2 +- build.py | 33 ++-- compose.py | 2 +- deploy/aws/values.yaml | 2 +- deploy/fleetcommand/Chart.yaml | 2 +- deploy/fleetcommand/values.yaml | 6 +- deploy/gcp/values.yaml | 2 +- .../perf-analyzer-script/triton_client.yaml | 2 +- .../server-deployer/build_and_push.sh | 6 +- .../server-deployer/chart/triton/Chart.yaml | 4 +- .../server-deployer/chart/triton/values.yaml | 6 +- .../server-deployer/data-test/schema.yaml | 6 +- .../server-deployer/schema.yaml | 6 +- .../gke-marketplace-app/trt-engine/README.md | 6 +- deploy/k8s-onprem/values.yaml | 2 +- deploy/oci/values.yaml | 2 +- docs/README.md | 8 +- docs/client_guide/python_readme.rst | 4 +- docs/contents.rst | 1 - docs/customization_guide/build.md | 23 +-- docs/customization_guide/compose.md | 9 +- .../quick_deployment_by_backend.rst | 1 - docs/index.md | 6 +- docs/introduction/compatibility.md | 9 +- docs/introduction/index.md | 4 +- docs/perf_benchmark/perf-analyzer-README.rst | 5 - docs/repositories.txt | 1 - docs/user_guide/custom_operations.md | 61 +------ docs/user_guide/faq.md | 4 +- docs/user_guide/jetson.md | 6 +- docs/user_guide/model_configuration.md | 47 +++--- docs/user_guide/model_repository.md | 38 +---- docs/user_guide/optimization.md | 119 +------------- docs/user_guide/performance_tuning.md | 4 +- python/openai/README.md | 9 +- qa/L0_client_build_variants/test.sh | 2 + qa/L0_dlpack_multi_gpu/test.sh | 12 ++ qa/L0_infer/test.sh | 3 + qa/L0_lifecycle/test.sh | 2 +- qa/L0_python_api/test.sh | 5 + qa/common/gen_jetson_trt_models | 3 +- qa/common/gen_qa_custom_ops | 2 +- .../gen_qa_dyna_sequence_implicit_models.py | 12 +- qa/common/gen_qa_dyna_sequence_models.py | 18 +- qa/common/gen_qa_identity_models.py | 18 +- qa/common/gen_qa_implicit_models.py | 12 +- qa/common/gen_qa_model_repository | 7 +- qa/common/gen_qa_models.py | 24 ++- qa/common/gen_qa_ragged_models.py | 12 +- qa/common/gen_qa_reshape_models.py | 6 +- qa/common/gen_qa_sequence_models.py | 18 +- qa/common/gen_qa_trt_data_dependent_shape.py | 8 +- qa/common/gen_qa_trt_format_models.py | 8 +- qa/common/shm_util.py | 155 ++++++++++-------- qa/python_models/identity_bf16/model.py | 4 +- qa/python_models/python_version/model.py | 2 - 59 files changed, 347 insertions(+), 453 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6b9d3378dc..0d01b1a996 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,7 +88,7 @@ proposed change so that the Triton team can provide feedback. documentation for instructions on running these tests. - Triton Inference Server's default build assumes recent versions of - dependencies (CUDA, TensorFlow, PyTorch, TensorRT, + dependencies (CUDA, PyTorch, TensorRT, etc.). Contributions that add compatibility with older versions of those dependencies will be considered, but NVIDIA cannot guarantee that all possible build configurations work, are not broken by diff --git a/Dockerfile.sdk b/Dockerfile.sdk index f7bbf64432..c517be2918 100644 --- a/Dockerfile.sdk +++ b/Dockerfile.sdk @@ -29,7 +29,7 @@ # # Base image on the minimum Triton container -ARG BASE_IMAGE=nvcr.io/nvidia/tritonserver:25.07-py3-min +ARG BASE_IMAGE=nvcr.io/nvidia/tritonserver:25.08-py3-min ARG TRITON_CLIENT_REPO_SUBDIR=clientrepo ARG TRITON_PA_REPO_SUBDIR=perfanalyzerrepo @@ -43,7 +43,7 @@ ARG JAVA_BINDINGS_MAVEN_VERSION=3.8.4 ARG JAVA_BINDINGS_JAVACPP_PRESETS_TAG=1.5.8 ARG TRITON_PERF_ANALYZER_BUILD=1 # DCGM version to install for Model Analyzer -ARG DCGM_VERSION=4.2.3-2 +ARG DCGM_VERSION=4.4.0-1 ARG NVIDIA_TRITON_SERVER_SDK_VERSION=unknown ARG NVIDIA_BUILD_ID=unknown diff --git a/README.md b/README.md index 48bd0baf1b..1bf23ead01 100644 --- a/README.md +++ b/README.md @@ -29,15 +29,15 @@ >[!WARNING] >You are currently on the `main` branch which tracks under-development progress ->towards the next release. The current release is version [2.59.1](https://github.com/triton-inference-server/server/releases/latest) ->and corresponds to the 25.07 container release on NVIDIA GPU Cloud (NGC). +>towards the next release. The current release is version [2.60.0](https://github.com/triton-inference-server/server/releases/latest) +>and corresponds to the 25.08 container release on NVIDIA GPU Cloud (NGC). # Triton Inference Server Triton Inference Server is an open source inference serving software that streamlines AI inferencing. Triton enables teams to deploy any AI model from multiple deep learning and machine learning frameworks, including TensorRT, -TensorFlow, PyTorch, ONNX, OpenVINO, Python, RAPIDS FIL, and more. Triton +PyTorch, ONNX, OpenVINO, Python, RAPIDS FIL, and more. Triton Inference Server supports inference across cloud, data center, edge and embedded devices on NVIDIA GPUs, x86 and ARM CPU, or AWS Inferentia. Triton Inference Server delivers optimized performance for many query types, including real time, @@ -90,16 +90,16 @@ Inference Server with the ```bash # Step 1: Create the example model repository -git clone -b r25.07 https://github.com/triton-inference-server/server.git +git clone -b r25.08 https://github.com/triton-inference-server/server.git cd server/docs/examples ./fetch_models.sh # Step 2: Launch triton from the NGC Triton container -docker run --gpus=1 --rm --net=host -v ${PWD}/model_repository:/models nvcr.io/nvidia/tritonserver:25.07-py3 tritonserver --model-repository=/models --model-control-mode explicit --load-model densenet_onnx +docker run --gpus=1 --rm --net=host -v ${PWD}/model_repository:/models nvcr.io/nvidia/tritonserver:25.08-py3 tritonserver --model-repository=/models --model-control-mode explicit --load-model densenet_onnx # Step 3: Sending an Inference Request # In a separate console, launch the image_client example from the NGC Triton SDK container -docker run -it --rm --net=host nvcr.io/nvidia/tritonserver:25.07-py3-sdk /workspace/install/bin/image_client -m densenet_onnx -c 3 -s INCEPTION /workspace/images/mug.jpg +docker run -it --rm --net=host nvcr.io/nvidia/tritonserver:25.08-py3-sdk /workspace/install/bin/image_client -m densenet_onnx -c 3 -s INCEPTION /workspace/images/mug.jpg # Inference should return the following Image '/workspace/images/mug.jpg': @@ -166,7 +166,6 @@ configuration](docs/user_guide/model_configuration.md) for the model. - Triton supports multiple execution engines, called [backends](https://github.com/triton-inference-server/backend#where-can-i-find-all-the-backends-that-are-available-for-triton), including [TensorRT](https://github.com/triton-inference-server/tensorrt_backend), - [TensorFlow](https://github.com/triton-inference-server/tensorflow_backend), [PyTorch](https://github.com/triton-inference-server/pytorch_backend), [ONNX](https://github.com/triton-inference-server/onnxruntime_backend), [OpenVINO](https://github.com/triton-inference-server/openvino_backend), diff --git a/TRITON_VERSION b/TRITON_VERSION index 7b28f253ee..c7cf1876bd 100644 --- a/TRITON_VERSION +++ b/TRITON_VERSION @@ -1 +1 @@ -2.60.0dev +2.61.0dev diff --git a/build.py b/build.py index 716b227c42..f432411133 100755 --- a/build.py +++ b/build.py @@ -71,14 +71,14 @@ # DEFAULT_TRITON_VERSION_MAP = { - "release_version": "2.60.0dev", - "triton_container_version": "25.08dev", - "upstream_container_version": "25.07", - "ort_version": "1.22.0", + "release_version": "2.61.0dev", + "triton_container_version": "25.09dev", + "upstream_container_version": "25.08", + "ort_version": "1.23.0", "ort_openvino_version": "2025.2.0", "standalone_openvino_version": "2025.2.0", - "dcgm_version": "4.2.3-2", - "vllm_version": "0.9.0.1", + "dcgm_version": "4.4.0-1", + "vllm_version": "0.9.2", "rhel_py_version": "3.12.3", } @@ -1259,7 +1259,7 @@ def create_dockerfile_linux( # stage of the PyTorch backend if not FLAGS.enable_gpu and ("pytorch" in backends): df += """ -RUN patchelf --add-needed /usr/local/cuda/lib64/stubs/libcublasLt.so.12 backends/pytorch/libtorch_cuda.so +RUN patchelf --add-needed /usr/local/cuda/lib64/stubs/libcublasLt.so.13 backends/pytorch/libtorch_cuda.so """ if "tensorrtllm" in backends: df += """ @@ -1494,7 +1494,7 @@ def dockerfile_prepare_container_linux(argmap, backends, enable_gpu, target_mach cp -r nvpl_slim_24.04/include/* /usr/local/include && \\ rm -rf nvpl_slim_24.04.tar nvpl_slim_24.04; \\ fi \\ - && pip3 install --no-cache-dir --progress-bar on --index-url $VLLM_INDEX_URL -r /run/secrets/requirements \\ + && pip3 install --no-cache-dir --extra-index-url $VLLM_INDEX_URL -r /run/secrets/requirements \\ # Need to install in-house build of pytorch-triton to support triton_key definition used by torch 2.5.1 && cd /tmp \\ && wget $PYTORCH_TRITON_URL \\ @@ -1554,18 +1554,19 @@ def add_cpu_libs_to_linux_dockerfile(backends, target_machine): df += """ RUN mkdir -p /usr/local/cuda/lib64/stubs COPY --from=min_container /usr/local/cuda/lib64/stubs/libcusparse.so /usr/local/cuda/lib64/stubs/libcusparse.so.12 -COPY --from=min_container /usr/local/cuda/lib64/stubs/libcusolver.so /usr/local/cuda/lib64/stubs/libcusolver.so.11 +COPY --from=min_container /usr/local/cuda/lib64/stubs/libcusolver.so /usr/local/cuda/lib64/stubs/libcusolver.so.12 COPY --from=min_container /usr/local/cuda/lib64/stubs/libcurand.so /usr/local/cuda/lib64/stubs/libcurand.so.10 -COPY --from=min_container /usr/local/cuda/lib64/stubs/libcufft.so /usr/local/cuda/lib64/stubs/libcufft.so.11 -COPY --from=min_container /usr/local/cuda/lib64/stubs/libcublas.so /usr/local/cuda/lib64/stubs/libcublas.so.12 -COPY --from=min_container /usr/local/cuda/lib64/stubs/libcublasLt.so /usr/local/cuda/lib64/stubs/libcublasLt.so.12 -COPY --from=min_container /usr/local/cuda/lib64/stubs/libcublasLt.so /usr/local/cuda/lib64/stubs/libcublasLt.so.11 +COPY --from=min_container /usr/local/cuda/lib64/stubs/libcufft.so /usr/local/cuda/lib64/stubs/libcufft.so.12 +COPY --from=min_container /usr/local/cuda/lib64/stubs/libcublas.so /usr/local/cuda/lib64/stubs/libcublas.so.13 +COPY --from=min_container /usr/local/cuda/lib64/stubs/libcublasLt.so /usr/local/cuda/lib64/stubs/libcublasLt.so.13 RUN mkdir -p /usr/local/cuda/targets/{cuda_arch}-linux/lib -COPY --from=min_container /usr/local/cuda/lib64/libcudart.so.12 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. -COPY --from=min_container /usr/local/cuda/lib64/libcupti.so.12 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. -COPY --from=min_container /usr/local/cuda/lib64/libnvJitLink.so.12 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. +COPY --from=min_container /usr/local/cuda/lib64/libcudart.so.13 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. +COPY --from=min_container /usr/local/cuda/lib64/libcupti.so.13 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. +COPY --from=min_container /usr/local/cuda/lib64/libnvJitLink.so.13 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. COPY --from=min_container /usr/local/cuda/lib64/libcufile.so.0 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. +COPY --from=min_container /usr/local/cuda/lib64/libnvrtc.so.13 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. +COPY --from=min_container /usr/local/cuda/lib64/libcusparseLt.so.0 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. RUN mkdir -p /opt/hpcx/ucc/lib/ /opt/hpcx/ucx/lib/ COPY --from=min_container /opt/hpcx/ucc/lib/libucc.so.1 /opt/hpcx/ucc/lib/libucc.so.1 diff --git a/compose.py b/compose.py index ad177d56b2..e2321cf945 100755 --- a/compose.py +++ b/compose.py @@ -298,7 +298,7 @@ def create_argmap(images, skip_pull): dcgm_ver = re.search("DCGM_VERSION=([\S]{4,}) ", vars) dcgm_version = "" if dcgm_ver is None: - dcgm_version = "4.2.3-2" + dcgm_version = "4.4.0-1" log( "WARNING: DCGM version not found from image, installing the earlierst version {}".format( dcgm_version diff --git a/deploy/aws/values.yaml b/deploy/aws/values.yaml index eb9afb78e0..6ec8264ae1 100644 --- a/deploy/aws/values.yaml +++ b/deploy/aws/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.07-py3 + imageName: nvcr.io/nvidia/tritonserver:25.08-py3 pullPolicy: IfNotPresent modelRepositoryPath: s3://triton-inference-server-repository/model_repository numGpus: 1 diff --git a/deploy/fleetcommand/Chart.yaml b/deploy/fleetcommand/Chart.yaml index 1b1af554ee..5aebbe4957 100644 --- a/deploy/fleetcommand/Chart.yaml +++ b/deploy/fleetcommand/Chart.yaml @@ -26,7 +26,7 @@ apiVersion: v1 # appVersion is the Triton version; update when changing release -appVersion: "2.59.1" +appVersion: 2.60.0" description: Triton Inference Server (Fleet Command) name: triton-inference-server # version is the Chart version; update when changing anything in the chart diff --git a/deploy/fleetcommand/values.yaml b/deploy/fleetcommand/values.yaml index 03569ab343..94aa860e77 100644 --- a/deploy/fleetcommand/values.yaml +++ b/deploy/fleetcommand/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.07-py3 + imageName: nvcr.io/nvidia/tritonserver:25.08-py3 pullPolicy: IfNotPresent numGpus: 1 serverCommand: tritonserver @@ -47,13 +47,13 @@ image: # # To set model control mode, uncomment and configure below # TODO: Fix the following url, it is invalid - # See https://github.com/triton-inference-server/server/blob/r25.07/docs/user_guide/model_management.md + # See https://github.com/triton-inference-server/server/blob/r25.08/docs/user_guide/model_management.md # for more details #- --model-control-mode=explicit|poll|none # # Additional server args # - # see https://github.com/triton-inference-server/server/blob/r25.07/README.md + # see https://github.com/triton-inference-server/server/blob/r25.08/README.md # for more details service: diff --git a/deploy/gcp/values.yaml b/deploy/gcp/values.yaml index 55d0919205..027cf85819 100644 --- a/deploy/gcp/values.yaml +++ b/deploy/gcp/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.07-py3 + imageName: nvcr.io/nvidia/tritonserver:25.08-py3 pullPolicy: IfNotPresent modelRepositoryPath: gs://triton-inference-server-repository/model_repository numGpus: 1 diff --git a/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml b/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml index 951f04898e..842362d6ce 100644 --- a/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml +++ b/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml @@ -33,7 +33,7 @@ metadata: namespace: default spec: containers: - - image: nvcr.io/nvidia/tritonserver:25.07-py3-sdk + - image: nvcr.io/nvidia/tritonserver:25.08-py3-sdk imagePullPolicy: Always name: nv-triton-client securityContext: diff --git a/deploy/gke-marketplace-app/server-deployer/build_and_push.sh b/deploy/gke-marketplace-app/server-deployer/build_and_push.sh index 659e3a40eb..8ff6eeb631 100755 --- a/deploy/gke-marketplace-app/server-deployer/build_and_push.sh +++ b/deploy/gke-marketplace-app/server-deployer/build_and_push.sh @@ -27,9 +27,9 @@ export REGISTRY=gcr.io/$(gcloud config get-value project | tr ':' '/') export APP_NAME=tritonserver -export MAJOR_VERSION=2.59 -export MINOR_VERSION=2.59.1 -export NGC_VERSION=25.07-py3 +export MAJOR_VERSION=2.60 +export MINOR_VERSION=2.60.0 +export NGC_VERSION=25.08-py3 docker pull nvcr.io/nvidia/$APP_NAME:$NGC_VERSION diff --git a/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml b/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml index 218a45c7a5..04d80cbfd4 100644 --- a/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml +++ b/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml @@ -25,7 +25,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. apiVersion: v1 -appVersion: "2.59" +appVersion: "2.60" description: Triton Inference Server name: triton-inference-server -version: 2.59.1 +version: 2.60.0 diff --git a/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml b/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml index 94e5132d4d..7c7862d3f7 100644 --- a/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml +++ b/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml @@ -31,14 +31,14 @@ maxReplicaCount: 3 tritonProtocol: HTTP # HPA GPU utilization autoscaling target HPATargetAverageValue: 85 -modelRepositoryPath: gs://triton_sample_models/25.07 -publishedVersion: '2.59.1' +modelRepositoryPath: gs://triton_sample_models/25.08 +publishedVersion: '2.60.0' gcpMarketplace: true image: registry: gcr.io repository: nvidia-ngc-public/tritonserver - tag: 25.07-py3 + tag: 25.08-py3 pullPolicy: IfNotPresent # modify the model repository here to match your GCP storage bucket numGpus: 1 diff --git a/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml b/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml index edfcbe164b..7a1774155f 100644 --- a/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml +++ b/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml @@ -27,7 +27,7 @@ x-google-marketplace: schemaVersion: v2 applicationApiVersion: v1beta1 - publishedVersion: '2.59.1' + publishedVersion: '2.60.0' publishedVersionMetadata: releaseNote: >- Initial release. @@ -88,11 +88,11 @@ properties: default: 85 modelRepositoryPath: type: string - title: Bucket where models are stored. Please make sure the user/service account to create the GKE app has permission to this GCS bucket. Read Triton documentation on configs and formatting details, supporting TensorRT, TensorFlow, Pytorch, Onnx ... etc. + title: Bucket where models are stored. Please make sure the user/service account to create the GKE app has permission to this GCS bucket. Read Triton documentation on configs and formatting details, supporting TensorRT, Pytorch, Onnx ... etc. default: gs://triton_sample_models/models image.ldPreloadPath: type: string - title: Leave this empty by default. Triton allows users to create custom layers for backend such as TensorRT plugin or Tensorflow custom ops, the compiled shared library must be provided via LD_PRELOAD environment variable. + title: Leave this empty by default. Triton allows users to create custom layers for backend such as TensorRT plugin or custom ops, the compiled shared library must be provided via LD_PRELOAD environment variable. default: '' image.logVerboseLevel: type: integer diff --git a/deploy/gke-marketplace-app/server-deployer/schema.yaml b/deploy/gke-marketplace-app/server-deployer/schema.yaml index ab21637bf3..607aafe4a5 100644 --- a/deploy/gke-marketplace-app/server-deployer/schema.yaml +++ b/deploy/gke-marketplace-app/server-deployer/schema.yaml @@ -27,7 +27,7 @@ x-google-marketplace: schemaVersion: v2 applicationApiVersion: v1beta1 - publishedVersion: '2.59.1' + publishedVersion: '2.60.0' publishedVersionMetadata: releaseNote: >- Initial release. @@ -89,10 +89,10 @@ properties: modelRepositoryPath: type: string title: Bucket where models are stored. Please make sure the user/service account to create the GKE app has permission to this GCS bucket. Read Triton documentation on configs and formatting details, supporting TensorRT, TensorFlow, Pytorch, Onnx ... etc. - default: gs://triton_sample_models/25.07 + default: gs://triton_sample_models/25.08 image.ldPreloadPath: type: string - title: Leave this empty by default. Triton allows users to create custom layers for backend such as TensorRT plugin or Tensorflow custom ops, the compiled shared library must be provided via LD_PRELOAD environment variable. + title: Leave this empty by default. Triton allows users to create custom layers for backend such as TensorRT plugin, the compiled shared library must be provided via LD_PRELOAD environment variable. default: '' image.logVerboseLevel: type: integer diff --git a/deploy/gke-marketplace-app/trt-engine/README.md b/deploy/gke-marketplace-app/trt-engine/README.md index c80da18f89..4651799daf 100644 --- a/deploy/gke-marketplace-app/trt-engine/README.md +++ b/deploy/gke-marketplace-app/trt-engine/README.md @@ -33,7 +33,7 @@ ``` docker run --gpus all -it --network host \ --shm-size=1g --ulimit memlock=-1 --ulimit stack=67108864 \ - -v ~:/scripts nvcr.io/nvidia/tensorrt:25.07-py3 + -v ~:/scripts nvcr.io/nvidia/tensorrt:25.08-py3 pip install onnx six torch tf2onnx tensorflow @@ -57,7 +57,7 @@ mkdir -p engines python3 builder.py -m models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/model.ckpt -o engines/bert_large_int8_bs1_s128.engine -b 1 -s 128 -c models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/ -v models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/vocab.txt --int8 --fp16 --strict --calib-num 1 -iln -imh -gsutil cp bert_large_int8_bs1_s128.engine gs://triton_sample_models/25.07/bert/1/model.plan +gsutil cp bert_large_int8_bs1_s128.engine gs://triton_sample_models/25.08/bert/1/model.plan ``` -For each Triton upgrade, container version used to generate the model, and the model path in GCS `gs://triton_sample_models/25.07/` should be updated accordingly with the correct version. +For each Triton upgrade, container version used to generate the model, and the model path in GCS `gs://triton_sample_models/25.08/` should be updated accordingly with the correct version. diff --git a/deploy/k8s-onprem/values.yaml b/deploy/k8s-onprem/values.yaml index 84d6c62f28..402a52635f 100644 --- a/deploy/k8s-onprem/values.yaml +++ b/deploy/k8s-onprem/values.yaml @@ -30,7 +30,7 @@ tags: openshift: false image: - imageName: nvcr.io/nvidia/tritonserver:25.07-py3 + imageName: nvcr.io/nvidia/tritonserver:25.08-py3 pullPolicy: IfNotPresent modelRepositoryServer: < Replace with the IP Address of your file server > modelRepositoryPath: /srv/models diff --git a/deploy/oci/values.yaml b/deploy/oci/values.yaml index 093791e1be..7261b9c0a3 100644 --- a/deploy/oci/values.yaml +++ b/deploy/oci/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.07-py3 + imageName: nvcr.io/nvidia/tritonserver:25.08-py3 pullPolicy: IfNotPresent modelRepositoryPath: s3://https://.compat.objectstorage..oraclecloud.com:443/triton-inference-server-repository numGpus: 1 diff --git a/docs/README.md b/docs/README.md index a9604c0eae..f37ff2cc6b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,5 @@ j@FBNk+(OYEvhS4baG*U z`evfO$&~tw(3<6Vt!htkWRXT+BL ziwH;FgcnoAThbRxA_(D`NYM`{$~95;Ik-sJ&J5(2Or|d)4tuA-6&OON8;i41LVA04 zG2dj+W&^9K@wUNgo8$f&cqr*HSnS{c`}7YMf$N6?1N_%x;8@qUqT$|d%mOiA$J>yD zp3Q7a$W<}s+N`99WZNDF+xD>W?cgwqiv0t3>6yxtMc7dQy-8p9zSy)PJe=4FV&?kg zkZrv2hl)e}g;g)yAHZ*UdphB8c1c?*;5~3Q*ZuCIM7#3t5*)(EtMeUR2-b&6oy|O} zoZI*g;i$wb*LJ(Mgx)(4J1KB5G$dvJ0$e7-_4{IHmYFxAHs!PWwUWQPB*}0JF4~L8 z>Ph~K1(C(Q+(*F|#(5ETJ?Mk{DDT;#v*5)9?&RqyLiqJ4DMALNfjwdH zBJC}$`N3&wSbmN1QFZg{=Z#ZGY*Mv@*t=@s?W(KDlU6n(ib46J!Kh9qxPPtAxXL2$ z-39AK52;GuA6RYhKsY}kH*fbuY^PG@TfrwNb|vO;S?pkz8{!{q zK0#KS81CXC(c7z|>U$t19>5T1adkzvQ*$dFRd|XL> z8POuGD80F>H5_SPrFy3W5&9;E{MaD4+5+y|wV$u1w=Z$-Nwd1B`55S61vR!0_{SlY zgOGh*L^mCn=x9@ZKKle#HJI#?%o3M$^!c&J9#|DZGgwsDkSp4mN}i zcl{u1O}=7dLZemsi*c~lJyHKvju8zUZI-C>57ooor7I%`ZcI`+lv$XRAK=~xJIW|e z!9jUA_&F@fTINE$>LNrKE^=+m60Dl7tq^>SOnDi`uUR+F9SkaF4YbcI{=M~(#}@qF zI%|Oa^FBAt=(u*#nD%0`&%GPdG291Rwtx`5P}L4`u=7o8RTKP}I#SC^a(K~pc&}jR zs3Yq0y^YM3iO}lU5XZh8w>^>_@KNJAj6@*{*u4!f?Wx)#`~2PA)PoyTMb%aJu``qz zjmT8)VSd&!FU^d zy*W;8vq04_>HV&4ROUSJ*I4Y_{55N4=k~twrS3pn*372u)h=w=#P=BL7-X2x3}qa; z5M|8CzAH8h>s@>ke^=H_?3bd7V7UI+=E9mJRbPcEpU<%C3wp~

SL?%tKlLs6wK%4S>`| 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

-NM`M__{V*pG5q{Zhz< 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%0AP*7|h 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ᗙBXD+_$ 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

l9_V#WoQ3}&5Uu4TB# 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 z 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 z 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
T{!sN+4)a1Tc1m9MONef@ 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%0D4}h#-qIlVq+k715%!@6f(D`dkwzN5cw^nyBFq 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≺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

y#l&e&?1zk5?tx7mYw&4%KiweR zo%^$=hjALZKu|H)r^-O-(|2ryMRtNP675d=yXubeSsaM&TZAQ_FEKnKY8tasfXDml zFvyGPd`j$50{XA78S6&|lNC^%`3fUm0tF>*w14={;FPY(uk?8|2dL;6hK;-qI}3ZTaT;9aI<}xW*xhLv;M_ire3cn>z(I*b_ID zGD9RdAZE^C4~9DR#4iOP-o=_YEh4E=r_=ZF=ZFWG_MAQ*Q3vs7=b*IfJHG0p13ef5ZN;AMX?hQZHNt7`beb7 zDlZQl941e%%Zz-%SLW}MF7*{#^bdsH$8KYGFye}BU&k>!_9QoI^mitbyhck$#~|cf zb3fqdb_AhPrqt9!mX4l|(M+NNoY}dk;rk0S>x2=EJzQ41Qqf1Gp-Q#X2i77#k&5tUQO-&T3JZ3SWw(f^*I%ybr$Y>?pO4W@ z7*&*Ln9G%&{5E~QCmk68FgDD3!pvAs(xd1QC1tMs)=K~32mDiQPC`BEKaSht&eA*B z)Rb~pqvnN5IPzEg;ANtMc!L{hE4G{=y{a6-0U{%hj**lTXy$T9kg5Gd>PMo2)Gy$s zrSvUcb`ha6*N7m1RSZiLM^Kox{@c8U#cAW>jG4&>6sA#OUBepmm@1x>!Bg1Z?Bo`` zX0u18$MA6t4!kiO^`1^99YAy&0Rgpj-h5cn>IHut*KM13)@^g3`RqJI%3g=(7Ce1| za2Taix=$EsD9`nDCCi!*KF%z4+V=Mf9;_Q}p--Y^T#jYR_D!>N=Y4h}t@osoi~s;v zg-(h6(=L=*131_}kM*lXBzL)O=?Jk31WZ-~GCJ}{YDV?%ag6XPliEy?0E(0)G906m zq+OI(0kh|UF#ZqaGf5?VS}&jVS;CUCV+dNQrJUeO@1ze~G$2q^!!cxHSNBVJHb9x> zMk)n(mY`VBJ$3G)TG)n8V^784C`Ce(YvJBnvfs_iXqKQkiN2LL8rNu~hLvBIOUEOTx2-3(&BEhH8$Y%mr#g65x@i>$43}=V*>g^a{SPeAoR({-BDT zl3W}DZlghqJ27R<{y8aJ6Xiy+M@3{=6-=pEAJz*THAu&rpcxnY%;?JMKnO3mAAgDu za2*aJ(5(oeMY`1#_-IW|C#FbKx=QCHeJC*~8T|;Fp_dKbg0*s3qNRe7;l)P;pPAJI zcuN|7M=*~c7HM>xH-b=fFs7=P2|L7v5x>wP`;t!FBq@Y zmRNNkRNN`h$WM7z4`rK09GYwwtC?GycsyNYj-S7)(xo%@-|W<7{um0gN3qp`S#bQi z&WOn}K2h_uZ?vh?*at03dycl z=5IIUZ1J#|y$+6nl{+P2YcQlqu|{|u`1*nT5^VfjMx!By01ijQia{K<^o`6SO#tv; zx`d2Qyd6R!_P7NAD2v;*FBuY5edtnU_f+(bcI4@-Lfac~>gAkLYD-9J zr_X$NAW!*#Y4`S)0==Tn)=hy1zG!Pc_^Or>84bcLz{7B~chfI?LIabTpMAmvQYh|n z&lRi>RecHWewfb6?BeSfeBpBbo_a|T(r>@2eiDB(`9|@SM5iS$8&TkZk2WJy|A@tP z14uGK6AOo6t|b#=OQ%?Hfw_(AJPpu+2Y6lw4sV{()OBBO`LF@!n4o=uL1vAu4C$rz zf~&;Q?tCN?iZz{tAyJ?}5%Dw3-3WV0^($@lTSMmfa!V8t_K#UM*%^N!_sn@_VTMZM z01|3qU$)SBjgVpTj2W$RNVg_uR@q(LJgRDj%N3FOMgU{Ud=&fKN4g9;HNrIn^+!8k zUJlDJEv%=et59G^Pt=wkx1hrdZ~?Vs8M~yyYz=qL$KzxNm_4%62m?)9%h_YO++dVY zb^+lu@M*59>g!>n2q9cS!ui@lD{8X`ZKM|ExXlHWyq`X?#Kh#IFj|MZ&2&9^yjo@F z!{O=KaCmYyI6Ju*jk42hMAa!(fa(p&)BvfviVS=0z$n(^NSxYYki~TpjHyI@3>4!X zj}&);O4YTCiOrw@eJsC8s43g&L<`EgxSc<$`rsQ{6*tH1s7ZF>++};eR645x23Up- zL?X*V42Ax&OJ%lHO#3bhm9MeMy5tQ*+kl@Eaf}$N!2FwU9Yxp9<&yUaSzlKu?g!%F zS*Y0vkG?->okTZT3ARsfK8pYzZJCqP7mwM3x30y$@$k(*-4WNLx^`-JSoN^_!Xpxc z2k!};#s^|ugJ+iE(4qHcSE>+6e3Yr{rs^!>x6s7KGl+C2KHWk4r3ZO>8{~c^aCQHA zVJcocpdd~1wCW^%OUL#rkD6wgvJs5MajzgJyFub@$67yl}^l_!8fXBod5a*M><3O$cA;-Y$|R8n~V<^Um}_*+Z%Bu=X9u>4|h zBq*~7rA3=vWGU?5XP#CtSDbV^pTA6I zmejaI<6oi_F%!Au!>^R^|6EmHkkxG~q`|9e@T;MNgWHRRH&Yi8(9gEz3aBD=jygZ5 zp^6Nj`Em#T?Z6m=TB6HoL*V!AF&rkl3xD2DH;*v`uM>6qCXKYtXd%OY?54m2we#j_ zZSmN{KHg~todKsSuMyQr{qR2KQ%d1A?*MXc3!ON z{4S!#58&^PImrPSmGcB4mOo;I?{c|7^o0)ee#smMVuUrFG1lY`yrBoH2Z>@pZ{XLi zFvS_`Yln@~Du$>^mh1hZUZ3n@zSz+a*}0l9kRC2@u0E(yX$UqRVy}OVaPBx&%u#JT6}riUhM-%j$w!ou z+GR{>_K7uOQv&xKEPL+HOfq56oj|D!{!rH8&c-T_?6!5kWY@ZIeOzhJZ#U1EMzY#fz#VCr}6CO^W zUyqd<_qqe5H!mUIfD=A@zAB%}WpR0PbNSEP$-l%Cn@NZD6+Gvi!yggJU#W1>7tis< zl|_Q93cmgeRJPbH2mT|xYuV_q-a(tB01QH5q_lEH9_56y0Kl6V{l_E?2PBy(i5+XB zVp!l&05ej4Ha8>+=5?VF_wsjLK)Prk6fQVYb}cF8AGX)AYz>hsLolvo!$`%!Lqf+dm==~1 z>?bk4X`PuJ3O=e#<+(@Qxkmt zpcaKic^39GtJp)-!nE|eQ@9QckqI)=+<=z_;4kjxcwymzv3QEnX;hpaJ7*WQ%P)XI zcZ%kb3PQx+J3Drw=3KpMSvH=$!v3BX%5tCpSnSYf3f){gsX9h6q>RW*n$1bJ7L0_S zVD3fbiS|4#qJg&l(|6Axf-Yuoyth1J0R!_CCt1}Mg-gLA$zz#Bz1`QV-0H6V5^XRxrIi12?PTu zobe=aBKcwFhCoS~)c;a-o_oCzBGb=*4_pA%9oVm7K^@Gv6%eo*yO+rMz8%rs zK?G$Ez8k{Q%@Xuo%A}_=6h6?D-3R^$f*XZ!8?-HQK=A81!SJ+U=mVsb?mO_a9h@%* zh25(V3-MMdSqw;M>sb9bOD`VphFQRAoXKvw`sEdgZ;IMHRx&m z-8Fx2JGx#?5V%D{RL7yS!tlfmMN+c^F+V%IM}AY5$b!C9keq&8?S$=ru5?Vfkga+d zD5=W1WV+c_Gmx*cxPB?5Eb)IMP{>))c38+;BAJ3(*=%1pZ!Iv&a&<+Tr(@uKH{>qz z6O!sKpmT+qJ!WD}f!r2I!7h==H0}WeVKAV3y1WFsGDS;?QVayq^9>T#uO#ryq#XSl zT!QOrxtKlk+d5_-8kNUwrGFrNr#>H_zn$WHFZR!3q4n)BCnr+A$y9S58U{cFqPq(6 zJmqjsa~}6nJH}Ru6guw`pRDYyb0?cVl~+MOUN*vHM&$SSaJVbXB3tezmuCg)kT=+( zn-AR4i#PVazxjY@k6-Vxd@q^AcLp&>94|IDcEOrkf^y{9+{#M@7C&iI%4Csz$rW3F zb9g|HTCCimC>i!y2AC5>V?aKE(#`w5i>FTxOx&s5%uF?ZryTCGd-PBENtE<}CMOw7 z>p({JB8#u&F_J4sDH(w#*#Hb2L5zy&;k*3s=~(86KV_g*^^dy?#!nfCxV)P88DHH$@We7W(Ns;Jtb#HC z9L~4tc8-BMA61I`+jKqs0wxd%7{E0Fya3&+dMe?U)I2cmZXE7OBR1xmh9_PRh+>?? zuQ$x8J>pRx#2;c`(?vS79#zX-@K-2=E{9*=ZZ-s7-Bk=<(U+{}|5$TI1?<+D05Ho5 zgGHVKvj+D^wjd*0aK>!B07a(XwLp=xf-OoCMShKZEs%Xw@r*RhGT@-X%>)=3&tu_2dwk=J< zp!T0_Pm4k6{+Z53t_$W0<2x`1_a&rhcr?v;A*hh8r}`WRo!X30nk>I-3eza6@MW*i z@b8!%54zH73Qhx}ydZjoPukGK#-nDF>`AArpVVwe&pGSoq(z-9Z88l;xgk+(TV zlKSJ;Qu-XCy3aQR(vZ*pD#wRW4f7#ga{sVrXwH!*)J7>^ki9nd0MXUEGCHFapS~ zMA3100K3BrHg>*|G~Y5B2ETl$MEBUfH-AXlby7_;&0n;$$6H1;r$q)AP6dMI84|C_v{bWNmfV`$!_K`9&r6)h$ukFuSYsO3Pi>|Mp9 z4M{{^3T%FGKwz2zs>0_ohbJ9AjmRLYw3_|W=k>Ch&h?l?Qa7nrs2uFT5=L~zA{=uby&M-Vu9>kDz}-05TrZ+?=EMUX>6Y3U3jVMdpVzuW zI-?fDx-LYGUmWgTcQZid?YP8!8|swUyQn|1=aoiwDyAI30dD1XE-=bWw5N@Qp*Z;N zf&)VGu9*R+wBWpwgO)nZpl0DRQM8rA%m{QML;4Ms7Kkdl{h`IvqWl(R zty5GCzfblwtbIVC7|7Xj{C!q#xS$*j#$Y+uW%c06SVb>G(pK4wUB_``=0OXlnd5A& z)@V?KUKPozU^ptxpd-n_xqi7@1ZiwlMylZr`<@-<)8Dw@@&A^Kd-}KS{-<5_QkyZwHMPoYh9M}2PF>Gs*G$?;;L zdqj(q(l_tbR&|X|%INOEu#dcDWEdq%Xbo29# z3}F>0S>Pl_4LgkWiy_7 zX02n-4GXw&CVCT%rD|FeQ)b%zX_W@D1*lqBR36ebaxK%KxAZnV4og5Nk}>nl(ZH z=#e9s_dB&D0^`fh$Ys;NtHWGv^AT+5TWJBO8XMT86h?4x9K8sGL0#1#mNX^XzI8|- z5%4`VTI#l4H+~n&Tg0=l7|wBY#lO7u67fKlh(EGa?f5=gG{xX1)kq?0bgI;SyT^3m zq>@M4THjHN$sG|PU>wd1$O^@wS$DTi8O+(LN%y?pgvF*r$sDgi2f<&1hykl1Izq(> zt`Y`GV78LU*)?WiL#6?A*r83ax@B2Xg*2`)lZg~7wz@p5;eP+A@P%@+C8Rd8#>uNu zzE9skF!{3<3wI(E1aVV@(PPkEpEFX=5jhb3VY_in@>Fh0$X*yHrBGw*XN*hQ(f>u* zWKm3!H0?t4FmfEsQJ{_=y{+zX;HXf;8G1`R(NknJxCg>j(w~uCxQ=4?2$m%f!-P$< zM239unWYwR{y+=QC0)8bJe9f=mr3dN<2LJ8Yig9{jcLY{2@CB4 zs6NRW_;B1}Io7W)l{QkHOZ%`aH|+udMDi9qe7#Nf9)#*NS$+#TcDd&&l4v3Z7k;RT zdMqvFG$377f#Bjva}$%_YF$fsf@yUZm-D$KE&{l7Ix9c0#ndBk z2IO5}{}9~htB{|qB*rWbqKRHS(6obHvBR-VB7JB{W;g5{5TgQuPznrsRwIjR1B45S zc!2>3iUWub*+V2@oSh4fa1W*BFB~o0Rbb(A7UplaJuOi!9xQAens8N-2Ruh1W|Om# zhiGW`)O7HZ9g8ng1FGSIeU`O%F(DK%cg{*0Zf#+clAHOu5ipItZH{VdrO9JZflPb0HwBt^2!c6;`$XM2)0N9r&QFz2TI6?{XK9~kkkpGL zJ6Ld>OhrV4m>?qj;DS8asc+8^*Dzv3_|s%Az>q6b*w-(`Q#4-$a3(2;XaH`xF+~#b z^i_`L(0{=?-~gvrIc8KG+$^mz?(+*fP?gPB0R+@`#olA8TugQvi+*!k_%c@h_sufa9z=|> zuK*N+GBii@jIZSp`7!p_CGv9tnZ90xmGJ`2M4Gkt`yDC#BFQ;N%OqU zZIK51rSl7KB{eBV%KqcOZT63N?s@U0-0#$U!MgHWtyw5uHDEs0Z?5P?wbcC_FjaE{FyJZ%{axmDcXd>-$OhUdehfj-ZDcZPT`2`R@ z)HP5lq}+YxS1}tlMnPO!cS=>u)Gaho5Ep!M4IyQ zE`TmYbvXseuTHTzmo8jFjH-v%lez{fUgukYC4fBIkUzk@Z!6B78aY2OS}j&IAbnE3 zIEaD(VD>r`dJn=OGB#;ySgcjgwPI;%z%)@i=zz7Ddy}r&{Iw+r^NgD?39^BfuZv+Z ztH^c9d?=G=(w``yJ3(Mape)q01nEHD6{C{ODXnDUyEoS{ujio@^fPiKs{U@=2-FDa{2JF zg5Uh=BTtQNzgk!!si}3K!yK$o_%Esq?~FmvKVU0r1n5yzWap<{dKT+S8u?90!Cpm2 z_e5#k&iRc9d>jYZYj~_%(k2){;92HjBn#NFJjdF+p`a0b8^FQLA)>U0mAEPQhwU1? zFS#eWCycwL(FGJ7{N;h2{pkFJofPNz^zJg3E4Y;Q4~_#XHFUu@8m8AB zF_H8m)BLNOd(3GmDQE5OYHc($0@Wk1Qkj#A(&!6!#?}PzAkEd1;EHB!A-KYiL|g;x zQBs^k1#yA!aq=rvQAuhGcp|04{BI!n=x4Xn-02-(hF^F}aL(N~I;hezvs6`{T(Mf)lcG5| zO4E{fn={X3Bn}n=opy@g-4oD8caozFCu!1v^{I5&7^#@)Er?%o+5BxYUpMM2e^7SN z5C6`NrlSt5) zL2x!bO$l>;=Jkj(0&x3vv4IGax^~uUp1wEe%hFf8CNRJp>OOfC z&o^N|jcY{bT&@ zbQn*z;VGa{rcu|vJ=DSTc-&XaGX!xFz-|zn1P4C#Rp87#Il|yiYu#uUim2|=w>*;` z%|OCahz>@`SC%xkv=+kq!Mkc-h_G?%RD%KxyiNI28<;+Sv|tPbFm3EQ0WhAV zUcI2Bct71OX2p(Ub5a5cJE~oBgD6KbT0e0NqD+0KW9gGy z@N;393x*CP*Mom>DZ#96fN&iT*Z zPJv9F;p5K#s`^%L-@xc$CNiblb!aZGM)v>PdzTo=@-9E9jFB-yHlKxrECF(lv5=9O zQr;U8`2ZGOk6YdOb#+xvW!7|$W$8LHA~G|&;*pMs%&HtAG-AVwH7vY<6$>`r!p2^h z6|iGx78XKi1REBX`Bol&=bZohapT^|$eFKOw7NSo9{2TsoagWSj@``LDxaPgk5TbV3LNL00>z7@ zu!Px}q9ZziB9>fur&e)$_i(DQ&|({ zw`-T~nKSI!Ad>EAcPijrmLU4UrBLS2g`! z+l6$lqxxHX!W|!VlW~Gd$$vzZ&rv}%Bci6uG%qcz)dBgq92Js;08++|xQuMmtb9DO zme>1+(H-UI%FnJE-AR9cNGTicDAtF;!eT8Mk(s0A_Q0T8y8GMJjtasi%(hu4#P z@b_#_)7UUrmW&QucjuT7W$~V2y--ws0I;6KI3u&E6kr!TTkgP$9k8rlEEY?tzE$@G zHD{sJ5Ha?_3Sc?_okV%1(-VrH-u_0Fe!zQ#JWyLqGB*(lEzG(p#!Q=hF>XXzk7mO) zx)@L+k8rr%PPD0rcr*$=Lv`w@X@wrxRlS?YG?B1D?R#QT3+{o~E_N?l>Cs_R!--Ar z>MFmj`K$E4#_5t9yKpvk1<>L-5bN+bskMvVg;6A=cO;eI)C3QK-WA1uZzo*$O@Uw9 z>lDw@c@?66_2qZl!jnf!ZZGwqTmZcc1@TTd)}YpMiLC9mCP!mThGb`tOX?kV?D^1S zy0bQ>>2fg+CCb`wAD}(LFX9?qgu}>S%;KjV86qxhRI@>@721S#Osm@SwqQEwcy1M8 zIYcv}`Eo&nfu=;O$?NHSO=UGdK;sbAIq8{j&4i)R8D~;ybwvVe3y3|5Qi=I2+#>%3 zB}22l1|A{7*}^z;Qvgs2$OQ!7HCb;N>N6KJ#PLq4t-n|*%TiT(5@%3|ilNYvM)|sS zqAx*`{V#T#?fz)FniQdhQuMg~m}xJtN!YaLo+v_MBe?`ZVu<9NyLEH@PCX9!ns%e4 zal~mfN;n_J#82kfc4q?&R3##03o(3l?w+V+~8>4b+Ni*&WUu@m>IcZ3;y1d zq;Ab}A2rJo$8sI@0DZaL^bDWG+*o=_TcQ<`5t?1qT(|U4hq*3MoP*rx<~?!BeY(4a zK9_!)Lc&ND!`~rIVB_%oEkLG$9o$nrxU4ms$)AUVy_bG72_v97J|(6+-2Wlyknn`W z21uA`?RCBb*tqB+`i-PNdgZalDna1k?HQr031m%q*9yr~YN2%v|8_hE>hWr6Y7$hy z{5x#|x^(YcmPC{aiYfjvuqPIq4gqejPj@C=%)-#vD+_5-guoJGQ2ZW6;if4I6Hel~K3WY_ z__VJSd=bYHPB@G}X0M7OBnuh0SXDqs`D~S3AJ~Q5k}|l>HI4*cqj*ukYk8u1E$vkF znqaRi=Mu%i_V68bI{>te#oYD+3ZUaf72*^g6|vhMkAdFe4&6-b{VNA0v>r&4cXtcGnhWkvGU67Qlv~z3!BeDX_eG#F|6Hs z2j~6!;rdf`#x?(WjhLwxo9ZUnQY#i6@2=tEn8t|~KMAgv58NwoH{KvpgQ2#F8lS-5 z(R9L`Zjk6@lN*cEQXKmP9CDNTYBs!vsySOt$oa1)psu?k{qw4S`nT~L1hQu}{qU4? zU5VRakJ?76tLoQmncbu68ip3NCIsy5;|$f7VDf{RM#Yz=^par?(QxLO;tBgjJa$4w z&PRs>M4_JJQ_1N#E_MIi1pOV(kghwMKAXdkncqf+UwA4V(1MvZe7Hp8@Hx`76Bkst z=4yjEi!CcRq%cA5k>)UZK=ae2Q}VL}>>g4URa3z_;-QJcWUGv@=Shb^xLo5kEdEM1 zrv1GF%)mrZwcanTCjd6McDOe4&15uipwgW(pLo0hV(Y3HDBE|UOgU|26&z^PscnXk zGmEveds188++cFG49YMaPHx+sE&Y+AlYP98h*a7glb37AX5zU4qYaAtqx$qR~5wNi-UOJ9bmU zU!}rVAFHGu$4mCLM8hX#vx^<>9k?Vls0C99J=Be^iRnSKtX30*15wo@smhR8bcBh2 zvE`{T0F8BqUbaLNKUDWUB>TN+u4Y6ju2;i3Bmroi$oQy__3x8jT^R5CNh8mYm!(m1 z=OBT`QXrKkgEA?*`i=C>`x03#QYRgPY=JkD29M?k#Lr}~@xfYD9DKCnM8#!OW%H)mjPR`Rrqprnw0&N-sYl8YT= zWT{rH_MjwmMBiZOGngmamI&$A&LK%al)d&Zc>_>w2t|rB$&J*CK7mjC0!3fa1E6~{ zPSPhw@F}UL9^2~nW{40&1@VS|lUBr*xMpgV=DozNK=B#CEHKspZ`zRcZ~>J+Q=tlH z8OPn&lUdPlRfV>}tn%V*VjzSEm8ErR&QQTay}&3dQ}!r20>NaWN*Kt?$!g73O82R8 zDlVs}t(!8^v%%rcYtS7d!23bIJTld*YJgImX|$JX`e-5N(zcc>@AX$VmBWd7UuX*nlNt7h5t5Ft55F&j$5KKlhQvyXjUG;nbLc z$g8xuqfg=<+eoG#w=c_$wnU5T?-C<$Zn;}*xk95QDibmKRKzz`uHTx^&Z{7PwS~15 zl2|Xr&e5!vQOBJI38BTP%DR1UJsDNX71{SdwZR;Dw@XyirzKt{$wqqYJme@;8Dg?y zgqKQ*fsV+9r)HQ+QF#-<%&lU`QcaaW%n{Emb?bu@rr4%XXav7T6%OV1Q(myC7=azS zE$t2WAjXLGmPNG`zSv8*Qoaq2h*b%lo=V$NI}M-o8HVL{HNCE1jTqQMIk*M0^-uuJ zP~kn^kLwcb+!@^EpeB;pkOasn8o5x%kA(|2Noic>DIX2&fFx!}ZmjB|VuRPsz}h7d zamL$P-~pm?cgVD810gbn%l?n^na*w?2zoaINRzoh%0db@@ckbDo;;VqKu`mksrWefUKrEGe%j2Yqgv(Xpvct1=cUA0EmR=ry+(5{DGOj< z7DK|iZB}=qO})JQ32OyLcDgG!ee70!z|^LW+i5gQ@5pD-P*nN=2w_@mJBL(A&{+Z^ z%B(o{l&DS_hKryZ(%jve(Oietg0P=StC+omoXo@|oz7oY6BfPjqY*cQkhk}DG9{fw zCB?oLBm>C>%?J@yRW$N1Ifc(LJm0G3kefYEO4cNOn$V>oGxCrno`cjPBNDXiNp<9r zlkLRk#L93uT z8_-BgAfe!vB&M!)Xy=Cw7^h)H3ut6eab_3QUf|os2^25sv|C^jKt#aUrY)fDaLCOq zZOfRofvKb`iela4b#>fj8&ztm;2`vC?tE3xm-kp-C5S{1u!5t?cdBu((9jroNUBCT zWXIGm!aM|Z^Yvt>uMCUkWuQrSkEii0T;%+wnIQrVxoAacQNq+4N; zARK@u8iQmQ-bFhAx57(hz^^bGr9j6bX-bsvc%8PJm3A?ZwiD;Ni&Xy&*oPIwHxf?X zUL_IJ7;m+QmnS)R9Zx~R2GfhJC{EYscbAeiq);404n1@(913>nq8Onkv?ikXOhxsd zVOglc$`*5W;<8buDJq+15#BC{(#Q;2YCbtT7A%N>!ap@@d! z^z+`D=9RBz6Syj1Kmk``Iy7h0sjX!Il|5$&Iu~f!8E&V0lf_yu;$khcO=Pt*9w}87 z?#712R;{o(=9e>=O(-6m+}2_|!B+Lz)8NWv1w1LLa6|1ScN)|x!-m#Xt2&2b$5Zp7 zDmofovqqlmGt^1#uAn#^ekER~NZfjS`LuMU>ywUGhC@We@JK&dtD-#k^k%xEoTzxQ z0_0%PU{v6-+n|2M7~!N+&qfmTn7AXyVo8E}$_tLiwTUW_Cm2bp^^F?c{l%CbgfJ0p8cgRno-Tlqo)zxf5 zK`-#76EK;LLUesUMD^`gXVgbuLAxjvR$3LbiH4FOK{D!=rxx;HYa_20oRG3uH1UTq4tK&70(g)tm_l`KNPGMazyHKo+Svx0V&D?@D;r3@t zQ@SMM8D@R=j;SRkmEzZ*QHx*C#Vv{=&%8JyffYvdrxh6Mk{4t}w=GR<^cF%7>**g( zs${3+@Sj|tvm1=cS6TU5} zSmq&dDAhos%@OoDtC7~;*4dmp`I+d)Pd|2SPDviCG!8_EK%q!VkRbkbaMeR)!EAj^w#3$A!0#z#y<-T z>r@%zpPK20h53;UkCfqZ^$8O0HA+Qq$O9lZ{ksvI0E13%3XtXnwrF<8M?5-bvRCOk z!h#KMg4r?Qh?w<}6qx&KO3-~pKhosYQPS{%-LU*28@H=cXsp@CXMsg_t~fg!s_%a9 zIh)6~jMxIQGpE2}5Mzrq3`w9hr2BHKq*}x%FOx3VlyD5n-i!5mdcCj#jkRhKr@l}d z%YKiw%D$D$s*Kw|-;{%z5MPTa`3JC<$426kJ2n_Cz&C*vzCgwBs+v~-1O%u)s>Rb`0#I5%e&h|ZX+DT9)dz3RTWBunqKF4ev%TDL>R56 zgddSOWhA=HuFKnfKB5wl{nd1IF}#iogB??Q7Zu*c;Pd#=yim1bQdLPjjI)5nT~EG` zf&EO7X^9x=xK%qD3kmJWShoGlEFX|Kg*XQIsW<|FlirfUDj)F<27om9lonu%q*BL3 zprnx8UOVBc@x+g?3Ta8DAIL5a$}pp_Z9SZE-caySXWzYE&jI7|w@~Srwq9^bk8^k< z_9|f+Q^6L=Iq+KOmo}ZT%p%2^u49U<`lbX;z=lwqcx(tNSo3SdJI<&|rce|*^rZPM z^h;A*RB}snG8)G%CN$4N0*Mk%t6z;1n?!z^D}$_(FkFzZ+FK$A=nH}iyhT%N5fg1z zMN?LTjcP}pOZAMKUU)obqGz)o5P7ylAa^tn%3~=?Kd(Oge)Rz%uQ}R75JmwAoxAxA zS??Kj!aBP@D~eYi*BoR+1G=-zkUvbNAH9dAhR}huOt_Aj9u+Di>I|ol?+G(GeG6)= zy1tr%9BWvdf>las`2nIplP^RBQn_G>qQ)bU*srHor$4S&6ymJw>AGHnUu(EJC5o)0 zy1pOIfONgD4r_sGz)*Na$;V?d6%u!E#FtI=vHkXgs+jvJw2M_r*K;N7w}?EfHyzRV zv79b71ea2&J!-1+&1`u&ocTIOBVuLI$7d;C&2PmFdsh)i#U%{&I}s!z-1vP&CyMk} z^?E_U<2o{h7?kl6BYZ*sg$CKNu+pj6yhKObH-6uhY~Z9Jq8Q38GoMLu-WNgT-a_`G zI|nkNplSOUjkY|)r^=OrvvJM_bLMuTZwa?=cmimECHYMHR2 z^9wVz9T})1vsjAK)Y3!dX+sJRk9vD_?3^ywrwPxw8ZbvK<%Y}pbh)PgsM1(S@qYZV z?mh35)QL&!?S(15#sbH5q9ZHCO3m$fA#IpNmJQij$Krmv_{bBAPB`EPd##uiG}E)i zv9*aROBE%(z5`V+yv^8HaGoc=hU-j)>%I?S#?8&r$(S?pJ0%Yk_nCc~-nQ;+iGuL( z`i+t0KD9mwIjpzOSHdNTQb9qfKK5An#FkR3eY`t9K4xNgY`-=%Zz=1r_wD z6`%OpfsCXU(zA(;;7A)`y?_!MhN>F;CNa=b87O*?l{+F-bPLyse&J{8)qsEeO3=&GIadxUzUMeht=w5uD$P<%`h@pBwzdZ0Q{2e4mmi7#~`h1q6J?Z(c-P{;f1JWw4#oU^r6JIU~|FvpruTo1gG`eQh3C+Dqfy~annzG)M}{-XF1Ejqa+m~c8 zLY?Rd3f!{GosIfF#Ogv=g~dy;X0 zJw%{Twuh2p!8ya0vh)N-E1ZYkae}KNAF)J`8Cz-&+}>t#A4HC0{b<&yAF)2ceUWQL ziS{k+dHe&Z)6YwA%T`lC6o$c{EOiY`c3A9bSWr{u-ma%k6h_HX%h*{a&RWjlpvZ>K zQ<2V4-V{e~5Cr15%EeNF38rUU%@w)Q<>AZKbaJKB%z|vvOi#P})H!xeFCp&Ma$q9~ z=d~+&^R?2c!Z_#1WMiW&JyUv~s<9(EYtfXfPEN>^ct*Rd`!3N>lFfce2DebT+~k=og7`@--i zVVU7AIin1lz-AxU$Wx+=H92wslB1>zpbw3a=FB9I;7<0~uDNYTUhlMQU)JsLK_^w! z`vkd=R4RkRI^lKjXWEbA#>kl!;i$Q;0nb#~igUWOZcZH(E`iWJoICX4UY4y}Wh3o; zkNa?YJDXrvmOXDj8vM&zd?Lw5M@!IE`^b<}8`y#yc^KqT5Tf5L%|Lo08p;0a2hg=n zN2(kH1tNE7B{G{(%W{s`L*;xJeoil~K04nKgC})H#e*+vB#1aWlJ{w2hscfV76)%Z zV$9c#aw|@jBp2p&Sz@e4JR&YO)u*zxd7XNKOt2!I+7RqgrpsI^Z#8(os22Cr)e`kA z2+m!AiH6d+6=?BkzWo++oprHa>+u9BavoW8-VWAHS}ePJ0&WcDjyOo8Jd#3a&eW>K z7zJTP{l!#*7w4P7c!G!z;C4=kp7`2T8w`8nw1&VWXA;Mbz{(KCCO~aAF|A>aShCUH zb1R*uoUw+et^kMpzDAgfnjtFa+22Nr23NF@j$=ymDLxuu^CHjmDY*e#JBu!~<%J;(=H4nOH&=S?R8#D^>1WIvXiPW%LcXA~OFYn%f znKPf!nQ09`yneTuz`q7T3c}c8`LnL}3>mfaqgBI!iM0GRSA8-9k?onrE-fhXJ37h4 z`lxtfzidb)go)U0F+|6aZ_!uPjb2;nJ8-LMYQ^N|vGJ#j8wHY%wwqlDW*}KUYI$*0 zQ7sUq5|xOsH&SST1Su!-nst7v;UYZ>uHyct3-Ep^QA`0~gn7HiyPVPNNVB5bAnFP^ zF~H;ExNzCU!s$pE)CwUGWKoK+NiicE$l6h=hBm!zLd1JDyr!9VB1B6$V#(|BG+z*7n=Q93*n8B zK$b)^nQ@I|McISf0ybMvNoZt|m;xfLID4VLaA=Q`LZnRRN4<@RX(PkL@rbtS@cpD( z?6@8Vs#ih2RT@9gy(u-9@!^r?0W8u63R0%~u>KnadS zgeeIQX2LrX^GVK&IgSF*t81QYDgk90^s2gK{9AUW`CBi;1C#U+{*Wx38Ie8El-dDq zf?WEGfrD9|nHd*wB_GLk?KNw8Nl0)tUy-w~0=i{E+06LcU`-mX4X4ji!bK0YXgZ36 zd;Np6s($nK{fmoWr20E68bRd%)|!}U&{ zdzUn0@8JAlG19d!;!y$70xLINX3;KwOUbWjmYou^Z~=q`V0wK;4w@UJFAsoZ(06XC z2{NIvC=QmyScdme9AIk`5j*i4u`y&tr;_hdE2ChjP0uYzYkPcoluQSRCDB4rxav!> zdntXoBE`%>uJzq~!gDH?nD#eVgE;0>)R%30#~J52a=9j35_~usqU!+)JmON|ezx&O zcd#Z^X?Wn{ot*WmXVBdCA$w(5v1tW^y3w3P2jW@7o#h5fobm65*hs7md!vGLnyhr> z8L3rnHKBGC@-^C91xv^ffp7Go;q>$+dT&$InoI3cLb~V^+aBxz1wHVJ#M3BCBMFPc zAEv7bcmd}_;;RB`7E{x%v+)PeqYP@LRry#2uno+hvb5XDHG>|XP_Tom3Vl^>Rje!> zuk!idij!-=T~WCv>QBss>Vp7Z?Fm&TC!DN!hJ$RfANP|P&XJua=hB4TSvs7KD@ZO1 ztb2BJDecUoa4k*13V>WXbEAYVV49+n#aalE*alWrX#9pwo1NXB?J)m>*5 z%;&JWt9OKa@wIl{lqAog5W*{(C2p}cqC^!r&t=u8RFleEAXsFdE$Z~^Kl+|vWKAlG zCOl^Ta1~1;-=N|#AyTauIVULI>q zpSowLRvGj+LPK~-vBvP{I++SfKE)Y>Lk(w)eehIkukEoHH%%P%$;_x6yC$*!hG-=} z`ZPh-8U-#J>B5JKW#XEaFUWEdnfzjPEhy$N7z}p;1g>ZIaM@FW`0ULkweqV_YbRgF z`Oj(faW=0oq?8xtf5rIDPjLX*aF`f`o7UZCNHWR{C0L2VJy9aApz7%^ z3C#7!*NRpGs7$|X#3D4ow}CdQS%tbd8$H*N8%JgY49SL&TzKUkv&ASQHnl%8QWdsZ zis;D?MyBoHEely&GqDiiXfTh1-8Wnqez8=s^=$RimKpx#Ec#ukm0+OXPRAkY#}RY6 zY6@lkvVcrw?!pDv4!gX~*iK@S9qS^@al`HdSSxKvmBq&eM@hMFhB_&LI`WovU#pL# zQPOY>ghzSIm7(HRz=XR$L}IY3ZHmjHUndNt6J2kpbQY)Gm{Bv*hFCtk8N@K%ao`2| z-K&d8ZJ`j;8Z6rsj?ol0{7cEdMmHZYe3r+QP&C=amk*3(Bvki15MybT?wW7A-8>1O zSa$*@q`7lKtHmQD$OR&V4=y?{8_Ct}nrc1;=O%uF!&!NDLw6L8&OVYHQOLkHpF9NQ za}2?aGOZMn}?(_4FkSw{RM`(1d4`&gK5vTw(_PinjMSoT(vdoj^l3;d#Eamf#FK#b%5hVI}Ow#xjEwi7Tzsj3gnt zWDFmU_qxnNc9_r^@u36rWOO`J3n=Zigs5RTlb~YeFJMIys5uo-!*M2{7LIN@3fRD-g6$n*}qbx?-RD4-%du;tLaEz$NpSp9x1j* z6eOJAfgz?$GC?6I%ikWmd=F6@mZqJn}l;j4M<*GW^c z98`-DD!QE7N-9~uB#Ycx=A7&a>(pp3lm}~eYi_5I9M)njL}{JTrx`G;o3bkoRrEyq z-F$!gE`770Y%{Dr#CwdZ01U4f+i21`=E`a*h^}!aI@Y=d0e?_I29dE4k5K=|`7FsSp>H&jE~Sd6+V~5LRhiI( zTE&o~-2MT3T}sZpq&zF0etS3<-6pOVclGGYm!h|re6G3pia2fK22(m?7hAq9*8N}w zVEed|K6>%e1Lcl_2I`8+Bqxy`-@@zvjdmHBI9&&qw#}XG%fqQg2AkFf~AMVO)XpeRL7}gIZ8=2xi#@zt*$-npRCov@9dv8R<&2yC*F|K-49kc znvW#$s8;SUd=AZKuaRS#Zq}z55IwfKtsF+@U@SG6FpH}X7#3N$xFwD@a(|K!< zr5muXs2x>oNy-|?{pbi~OzYCVNJ43wS+8U@yQx$g8hQdnoiJ_q8jji4a?v(r+?%Y+ zw9ywmnU%d!Znr32FKv8u7g@qlD;BgK{A5SU*tEt$@&?Z1_2ex=A#Bl0566ooX}GZ% zYbvEev?u(j8{Xk*afC@0Zgh-FH;IkHa*&HuXE!k#G8aeU3wb`pA&IBgD{7A?Rlo%> zbHEjWb!3Hy!pbEQ!s_f{K7V+ir?--qoVC3`jkSFg*$Q@H5ad~;1C<3hwq!T*3okae z%W6Wvt2J9mqgi9*D0FOlNvbVYXRSO%E;)vX<@and)b7h|Bm#0xH@gSwy5NuX!qRfu zt|#gX1P_YB#hQGKg3r4w2@UmbTD>Mr4V_R7oERtJV^nhxE0z;2KVoP&q*-VMn8%|z ziFD~xER>xFtji3FWDFlP40_F~!<8<0Pei@a*`@N}<6?NXxd9K!6of9QR9dY7#BS@{ zlPt7TR%VgLY_m%qpK>cu1L4z{h&Dt)rgI871McQ3d7`bl@HC0oUfD-9gkzj^JV!>< zNkkh-K^T8-T~HNgFPoG9;ng#xb$SOzKW_-Xa3cc3HNSk|RL*XJYzbnX(0S~$*{Ef- zTf;SVRUM9;HPw2mx4LIq7!eXu_uDNG^h9x_VzoqAI%&^mCO44CGGjbmowrD{I_2#+W^PB0!7F2L5Lhlp-wdmIk- z)`DskbmYcPWoIaXvA&34{45HCW)Rnr`5Zok5!`vY+7DFl#+^p_Kn|gWq6MC2kVj{qC zTvFl@yTyBj!p04S2 zTyL;E>e2FUp;{xyGO6#gx^)W@CXL?5cDvejqlRt9I?$52B|~;C?8ev7r1*GC!wJ|d z7plWOyBL})i^PDv{4phB7$QQxk&cJVu#RUE63mLv^6>TD?ToFtQ*?4#yt|u$J5(HI zW%0PKIw_PUGdeGg2Lc9jYjxn61E|13V}U81T`~^}*-HO;c?117x!-=8M8S`fs;$d5gdV!>+WUzw! z+H*Fv&(7J(W@ffEX)&Y#k_lzy2h|$22yGf6-&Bk-<0@KCZIi0xxJqHXnluvr)$dO$x=w{NizT?NGdFI z7ouCdSE(^HJlJMEW6MBxhC}22T(E|C_i51cNu6%NTOK22PbR`GTIx*3B4uidI}l#L zi`(F8#;L+KQ4JhIVGNIxdUZQGyg|}(b5q_I#qqcNCp(*}*mgTG!tvh>`bmIKuZE~$PejA*hU$qU^TTj8 zzq?IsL8Wq>YovReC$18X?v~owoc%!Pg?&&L_#}U^N42NR^nuA7iUFdxfL&HLZK|Hh zfOzbb6vqUsK_vx}TG%m+VDk&9M8nx5294Y-a)y?3t6In@I7EAJPCT4Q;&M_^89qtm zXf!}QfL|E;^v!H}iI&K4gGr8w;GmcS;htFX;xGSOj>XhPKlqf4vbesna3j)5Iqxpk z6oUxtNsh|BfsaPco}l)3u?KurSxD$cVGorIXrBHSu@4>Fv};E3ep;%XD9UGl+liMm zPW8knEW07HiP1fQ?EcK1)hu8J`Y)&FctP6XSK)5ut;=>6cU_Y3;;&od_w+q5EocZ` z-+^O;R#zy9?eOmo7Mtw`7Bdkrdk-K_o$_9${8Cvs;jl0Y7CuMN@5PMc=;wA&?($%8m*jcm#i(YA-*yPMoi&Xx#?Jp_T6Ipd$Ce$V~Ywvc|m zdJF#(;=gj}Q0-PL3(>v0_D@q+Z42y%%dPpTJCWAz9CK znV}pCWZRK=hMgBc%A9FiV{%m;o1je)GD9YZ;`&niWI4KuKI}2t9`&s(M)562=mIkT zz^zOh#cbMDd=$O;fO17{zm$)Q*>X761U<$D3R7zd@3Ns=qKOzJ@O~OKe|`tl7~*NG zjaXndwGm8p7e@9b&y9xq=Obo>LlA%#QY*^nA9Iw!;Sfw%s7H!Uk_It>ku(D4$*BK3 z8GT4pCa1-%!U7g@a(QUA9D&c#)$3@Df(uIJE|9Y09)Ob-{sJCvIm-yNJU({5SjhaWSHAa zYViKgtlzz(_6Y!5!;ZS)W?D$WcgL#eN@6NBXvCAk(o%K6N<K)Pcw2PPHnQ;%hXvJBh zA<384qzk0BD73y@-#tOnRuqiZ5kbUCtNX;Wv~zB}VEG~bSk~nW@3YbWv#H||mdMBs zyK_}6BCIM(W`TukI~DH)YI2G;4gHK9cmi`~U*3L4^TrNSzZ8#J2i)wKZm)n( zln|uMQx!=yF)!`*p3D@5+8R;A*FQW*b@p@C`TCc|v5BBpUq%ydZpun3Tu))_p1L{B zrBz_v6qW1Rw^jmDs^m$%>%uc`kH zOpW<1x0r(->jhQjXh%%F(ZDCC&PzSofSM%YX8dyx7TIs=i-Y|Dt)+H>?2;=BM?0wOSXLsubXN8w5Qgz4;qZL*Q-vc~3^W}Y4irzgNmVW!X ztR+d};nmKDB*a+SkgW2CB!87QByqa!h9pu2=kSw&cU&OqqSZ>c!iiT(^J1PYm$O63 zGZ%*XNenq*Z~gU$wmDi*YC!gpbJgAY(`_M|9PMa;1t&?$U!M9LT~YI*C=3O3LYQi?JUi8K*z?065_l zj5%u}Lk`NKQ}}H^9;b8lc2AeA5DlM_oZLRgI)^jFiC3HDm|$jSXPJunZ3Ydmdb=^y zI!Txn@s5hj>`X^7ofT7TLA+fOA=>5*(ni_WD*qDeF&pXRI#yqbQ0LnWW``7zVO5beSw=EUm&nb}1m%Vw#R%z7^E; zT8Fx3Rt+t>Dqa(Si0Ii;K6$La2?bZ`jo=@oJ7CGEAcBGA&wgMw~n z+d5Y4V-tohH{?_hdwy7;dGScw(l&^SZQ(qn@a$SoG1#jLK}u?QKK(bRzSlZSEn!9>RclD&WL)5zATy=m=?65D1RFW#9 zGyaMTWl9C|JUgOW>Y1XP&h!f1hF2WcQNh`h^}B^)qU|2ash{^0H$hjLpR8Ri?9K`% zB!KL&Y`Yj=k?{8Ws6zryHcgN~lInG@@UefSL@4UT>fE$$a+aWCIRV)Bg3qDQhA?~Xna(bLSQz)f8&M%B!<$#v_iIWQn&96qLWjZuM0T6Z z%bGQn_fnsTZ{d+q+%5zrDkgh{DOxN;F8fe^gmw<<=V8-RcOrnEu6#kbV}W40&vPQ+X_7qjvs8tW7 z>-4lZEX9}~4NHsNrFeg=sYLU|83_95Ns=FBu6CxCOqVoA(&KZ6P}N0SR57yRliU5c zr&e(HihRRCHi&n&N-}lb#84V;Pq(f+{0{8^mv^htL^DKgwO&r(q`W}Q5qKv(|D>&j zDZ#HJcregCYZ&&oH^Xx>#*URTciKl{58xFeJbripf=z|7323@VM*mf@V7QIUPo8~FERnzvQ zNKz_$q?kDrE7gGEzGQ0H+FE$Oz_Q#Tp$PT?gywHIM6l|)mc6&j5r}#IaDw(}n;VD^ zC%svsgnINzBTC8?Y~d*^3%K3l9@#~!DjeP$-#X(pe&B167~Dx!{tPk z2Dy0Qq%)uF>l2>e*%2M8pMC1v%N?s3!h3O%N4j%Yam;Wdx6qWVds+iU)Jzc4zeG<8 zpExK@g1wJLvYq(cSr-NBJr`psGZ5cG!{1VkH248g{}6W4nB&BVQhNJSin36W2rNp1^(8u|x#%xa^PY7{}K`Mkfc< zB^zbmN(VTL@9XQy26%xP>Y}Ne2(Ypn)ae15bG4*PI^!WVuz}I)6+EbzZ1XT2VHhy7 z++`c~9N{qiLCB53bQk}go*@!Hi7F>zePRwh(KnBvq(dhPWoPO+rh9O#|CU*!g5sGP zrp#K~&P5n++S!utso;*Xf7*p~{?tXP#ymI!AZS7bka4`3H>)LyPPG#40cy}AZUwAB z5IhE9tn6&U*I6uR-keO#;(d1U9i9l6HIat7$qjZG< zg_2_0=Ay?2eB6NF^M-FX^;Wk+~5W6LEm2!5;^ zBIM78me*##JSF01VkMa4#P#8^4lb9=^`Bkyt=enAhg{A=FQlVa!@DXRjixPz&9r(o z-01S8TwI0FxT$|r&uZ3mOe#X<6qj7xQ!wY)Wc;^Zp&En*fln&=4wowe`jE~skd7pr z-KUyDAA926Y=9(~0Zj^tpCOGQ!sgtc@I7@;{d9K=r%B@4vivj{TrUgtEp~5ax9^L| zgwr2kSf!o4k-`l-(sHn489>v1PVK!gp)svcOhJ(xfRMRA7%{CVG)pKZJdO}(*GN4# z>c106*^iu}2o+z>3!wXV%({lUI$j(Jc#}Q4NOt?}BMb=511XCD7T}8n!-{+ev%6G3 zM&w*zDsGZi4A~XOr)vf2HjTY#md?Rfb+w*MK5b3B#h7z!2f0lbfaQ+OA(gyMz>Vo3 zHqsV;G)Ky0Fh@!%XK-&)XC8>B0eFcO z+7x-|0WI$nHHn#_k@sDeknI;2tu8N@ZkK$SD8oV=Tlxfsxd!rEqy4@wu{okN4vWp` z5&^^W936|C3IjeS^21K5zo!^JXu~2rNLr!0ZU*fjS4&3slp-}riM#MX+WaT)WqJhi zjOj*gM|r3)5Q*>duEU-NAMI;E$2kF4D|#!BW_O^$xjvuFu6RB?Bvcq_;MHlDQwv0+ z@~OSulk00vQgpS1#Db6%t!*!b{5DCm4{+C+mL7FlDeQQ(U|Wc?juzh@%F?WLLb>WkBLKj-k7>cS6@jV!L@JGpD{e`WRf>If<3#p`*%45Y1vyPYkJShgk zJ@r%7GnPO!PU+HvL>+$jAZoW(8{I95r}6R~F{d*3795q-hoL>vk05X+{Ky!%RN3-2 zLhVL=U@+5wC|KonnVCj7dfg`xJ}F5;><0!FKDLokPe9Ai4K3b{d?BHClZZ#!Y8U4a zPKPck1<3o!X*2L7@Ta9V!7;K@>p`JNtZmkUTeB%95fBu&X|ICQnJiCsu9Irm509J5 z!VvYdy8>o2)tt8#Y*4~@P8hta#ux}Fvok?l^>~KB=DLgKLdzYp=!Z`FSOQ0^cTAOF zE^Z`3$MFaUZduCZ-BrFaMT20&br#`LIBNEGvbf&dyq?TZy#r*Jj*&CG0BG}=8cUzv z6^JQ%+wBzB;SH8tiA#PaHzRcr`2BYjtU3cU>x^y;a-&~^<_LLCxL5VCF5>%yOHGCs zddPDVp6S);k86xby+}tR7u#I^11jM7_ z)bg@$G>jC6S!1V_UP_UKCnCa;*=d(ykMh&F)|3kG6h{VU`ghU~&hwOhB9tKvK;LgR z2*rp!jUc@0`~VDT(|VwV9d7+ZpU9$5l?G$OU7{lH4J{y*Eae`Nws1lv-g1nA@vQ{m zyvu3;w}Auem&!j1@4j8AAR^~ZM+U-^o5_V6gTcO=4OdK7pdfp1r^P{4!NXClm%$VS zUcrJ(OhoV?9eJ-nt@x(L2OFf$nKWy#Gp%S8Fx`vwkfS6*)!PlTsA-SA)$>kc=v0g3MBV#%l(kxI@Iuuw&^ zL#i_mz#S@3jAJ3Sg!1>-)F<-;wz4F#e*CfSJ?}p+3%Xak50aGqt{EQOy!gVOjhKo_m@BaLF z?wFy5CAo(I?Fe&e<%+4a-zy95k8D$5PKFz_j9Dqj-!7&=rPCq}6*^C5lD5b`q-h+o z+Qf$er^_bo>O6S6yjBZura}IVsJ*}#Y-+D3u_`Zz)#7nUR7LWnJb1sDEU&EP!w|%z z&+3%+^cIuP^>D^+&=N{gprnW&FNW8ESXU5vAy`DEwtmm@_$MzC?$2IN4WeRjN}0sl zLU+j!iOQx2Q14o-AB_iiT)W3E;rPb^e^PM5Vz9bI7Iso!!e^|nrW+V}(2_?Bw5h4V z1v>dsUxCSE_P}zgI9Of*q_N>5ulw2j?HmST#s4n`x2x$r@bhZIffh-owC&mtNkX*u zZx!XCN8u=rfs5fl|FXLc5~RYR)G+Ce*`w-)JjigXaPdB{-d)AC@(-kVW}a(*W`{JZ zoqSif0SWs^UDT!A-VO4F8=A2Ksya*NSg-m8jQ(llqX1%&?daAIi>C zxbz>lubtJsJ!hQC_B!L(-RNaPa>TSMy??{U-VGuf%{pjy@YR~32`JJ?DMS+yBpCWp@PUR>YS>^{Cro-gb4CCuGYYfwI!{c$6q0zIs{V zYIIzx*f#8m=>Zc=3diW}GsOSDnJ>9XwC)JlchsFVe4SrP2@-C~OQWE$*hK1VbQXOP}B{24u% z9L2_Nw9<<{@r`BX%(Eao+j3i?Ch4n37q=21>#jUX#_lJgwo}okgv?BDhuCQ-1dNU) zwwlls=&?RSfc<^_=JYLZv`}WI3r36L;t$TM8jaXb-+hxj&W!XEKz)3V(ttcdb<_>x z(-6n)za9SJgI%#yJ5QhRWS0j(l_P<5JDtGx^1Th*-8$r#-p|ER=*lPj{{EEv(c_HjUh$@p#Cdr2}W(da*>Yv0F(}QL`8d?KuzIg!^Nmp7Ir=wMndVw3J;pz3e%(hVB>e`IpFdCY=+nfU)dh zNv$UotoKD##qg2vE>cO};&gqwpol+gdOfP)^zT+0LGFdCLE%;rAU`ar7p3V8I5xlL z@z2o|Cqq$No8a~~6unzhXGOIAs#kZ^Sq^ke>4COl0UTFDa59ZPO~z8yE%bqitRS81 zGv3Ty#+yNn)kr_#NzxTx)yLT!oyjUFANe=qLF5WSP6)jJO5OM9W;|VmwO~33V-fih z5aRXZ3^bxXQtu`1=r^n7-EGE6^)|#-1y`a<@6~L$UcWX?+anp!LK+oEfKiMQZe3y8 z7E81OysAbLmkL4zX0FnXfdoh`7Gw)6;BMI6vgbuh?bUX32@*igpn0X9X7li6Pg(k} zL6&kuNTxRc5Wrzis=c@?jV>dKPRt#esaGpX9HA>Vy#669MsonCNM1UNs=*0kX(@50 zkc_)aMU9_3k~4*p&WuTMLZo6tENk)*>g6TT!R{1gAP`5egCZ_8t?|O!$>zZ%q9ge) zJBVeYf^CWjZ=?D5KqPKT6)})9U3w5^)u&b8mf}U*+6;WJ2YC2+IhQg;UBbxfT~{s^ zy_cx5Sy;F2I~re4XFNsxF;qAMDRE2y?y4`7(H)r^bjLujQ0>CHcHR_Cq&teD(Fy|1 zMe0qUm?ej3G&Wx@$ReN~Vw2a?`I=hW{r~{hXt)}aMHDWoaAIZ^MQ{zf@eH0DJ_^j4 z@_?mO(0gn@82aymC}?rw1wo}6D`hPm-ArcS6I>U?tDDK_6OGv2y7EF3ijGAdCme}w zt3HWc$}e5STwpVcSqvEA)bLiM4KiRxzifafoewvP%B2_>vrNvy25on>!uShg1=CE1 zPfkWc$d+wLiL5|z8}TOAY-yD*Z6~t2(ZgY~XNOL*A?2Z_e*;J6 zH2|d~5y)9t+}&1lw6w@_uML#NL*XnW8hE#3I*`DaZl=Q-Z7>3<4gr!TfF5x1%wM1l zfR5_{$>PcCn!S)yG@Ky14GQEVLB6N*WBBkEM#yN&vwOgP1)^_PdG8<>)f3SJWt&Yr zC5yQ2atawG4cc=BGw9~0pMK)4O1m*8gyOYt@AA;MkmS}hk~dgKsX0=`G!1cEuC|j1 zE+>HPwM|-o%?T*B_?yykTSkS2Bkx+&L#TUQpU;+`V=r2Yu{{eJs}V#KE;eCX#X`Nl zCG+#D9^F_13}6R+6SNCtGi6KK;mVryTbZZ#0fWe1-60Rn8JuS21`L)5plQ(%zUhr% z`n-sO4grMO)1&}5S4p~E`y>45t~sBG<3kcRI^TeNr6BKR95ta3t;Sd-_Q;qt`mi`a z^WnSs&O{AN6Uvmrt3WsvAwGNEpk_ms&Ob>SwWCG2zjbPV* zSP{$vXtNy<18h?wxlqBQ>7SMKRMBe zJP-O)gR%2r`LbCJA^JegkONV_pLl$y{&uYb6AIsYZ)j~IqpPX-0!BbXy-13f!7E%) zAwwGwd;uOi^l_l|9s^;$-h%uaq&GW7igs&3Ldh`=v?yAF;BgFChlrV7?uG zH+PdXr#sULDXRvzCcTs4*l^_cp%S|hYb*XYK+j+X!(b*pZS+hWPXbZX$Z-^^dRli* zDxvIi65(R0oqJ`nqX#(Y0e2G8o_EZ}0!bbzgC;hX%CxtyufKe$$&p?nv4iw%eSbhV z5zd6s3q}72%CNEP`WB+RJYwon^qNsN0@KQ0EG6EDjy-JD0|ms|hmMfit_Hg|y!Jj; zK^wzpkJz5@5zO!siUP*8+Y@RBiO9elp1scY6$5kW!T3m->rufOQhlPa9-7Ywt(1xK% zg#sxLs0%nH#NAAIF<_0vJz^#Z2Y$wOmv(>7Y=-?&flpEatzs~x7z4$?YJ@ysIhsbW zu4XQ+fvGaiJk49$H>qHX*Y7+It}+S>K|^3K?kMvVK=6N!kUc^0?@24Et@ zpK$TSi4Q`=g~33HWNxwE_%OsC{jjL}H^cSKDNb79?aLez@ns%~$X}f$k%%~57KwPE zk%)alFQCqO5Y$w(L6W*D!p6l$yvtZ#Jw>Fvb@ebV$jZ(PCMOjZP27e}e3iu;gs3>X z%ONog8;M|WtJ5EZSdqHT?l!JKXpX3KmM+J%0+fg4MIK6~6u{|7svUU`-l7(1vMLX% zFR0dAOir)VcWdC}D!1Pn-1!B$MYEbBTy(wDv+r^{(&k(NFeSV!RTBW<^O}fjXjgZC zY9%GYv;#kMWI&fEoNnYNgyZ+&Zec=)Z)QV9@M-6~Uts4TeYes^x^`VN*dKc&#}Sun z;Kf%DN>T2ZS@Enhz$mow_Na6z8F|qvbrh$rL?wC%x9+8Df0R9#`H#WPzMH@bA|>L~ zOhu3FdS}!JnYSkCpMW1Cx2Cy@@_IOh*|eIhR?7%v7H++CJ$5lTT_gU20iVnfxdVML zt?UwQ`Xt9sQx>o19F9rXz{g>|X4+y6=CjD44eyxDYCm%1~AT}{UkK~Ib zgc=;qyVYE&v;df=H*f4@eET2QPzOJM+IP!T(;mxu$Lp1q;H96nrBE?iZ=M)nf=@&k^h1M4f; zQYiN{Ui!PlbQTvk)B}Y`vu2<~Jg<8N;c#J}!6T$_CBz3N7@1cxkFUU~kGKgjV!mHV zBUec%CxK>WF9A)e$;OoD|899fYegsTyG7`oX(-DJDU3jpgjNvY@Q4bsDvwY1S5P=Pk`1+Ef#aL$&LRr>?X8R(%)MS)=i*|D2$zez%1FjB{vmSKs^25 zF40_YrDbZkQYqPT!W|7@vp5u;jBXw0{cyq& z19K>7rWGw5?4mywz?gzki)T&V&LM7H-)vq1_;ITvEP039IebRfdL=yv-3v6?frSdH zV2N2}gf!E&LFb@anh*6sK}$5ToDp@FxU~zAcl2e(Elwv&qni(6J3m$BaFZ#>34S`O z&|wJX$7(fvsLo~xUL@;yvT$;3Qsk#0xg7@*AR^f$&Yh&$Ykt{pr^rq`KF4X3{M{1r zRrS|weo`jIb6vg1n%3Ptc}nW+3)J3WT=3iFaIB1<{qCPV4(e5{B$ZDSe& zMh;O?YQK&mN@cOWLz&2WeRVgBZ1zY@!JK#iaC|yaiYUoXnq7q8FM#o-c)eigqdye; zqs03J{D;=Y&aM#rZCHC*<5;JiF}4NLQIdjkrDmCIT|xUiDAGJ?gBy%*7u6L6HX=iBkbaw=;-x|bK3#P-SD-N+j? zp0gxCA&LW~NZnA6;kdchtH}8^|n5Ai>0n(*h<9lGdWj|g>!E* zo&q$D9$v(?bzI>cK}C$W#iVzToW#7T2t!UO8_%BD{kCKfeul&z93SNDIb|Qmr0p|2 z6)AkDcp>~tNS4G{CwnIG_=bzzJW&W7JfT@(4P*g<=vN38<@%shBm-kOQy~*xtYx_A zh@upj+It(u$Fb9#%tjO7c3|~O==c^Kx7b%QM7`i)W|O#_C-z5afy)OmK|QI-Chy+RS(k~vqi zoYP@PxvuN6N>MeBmpHBa4?jL4n(g5Vg&Pa8Hnyxu_p;MrhqZ^^eS}kc3q*!y=1hRWEfSP`lU5NsdBzCdls&APdY}iTEN3FHF<0M;6lP=Gr3B5usj;zBFvKLP3Z5HraQL`mXbhM-8Y7F_0%$0n5k>K$0 z)S!J;%?KF=Ml-e33Z~l)_e!YhX4UkIfE&UPe4=p9ji5)*V0K~J*7@zkDD-*HWlZw` z-SJc9{CeBCPQwUeI=8;aOqh@J2EJ5F+D2(j$Bo!Q2YV}g zw7`54D9eu&l`=LiDodwOx-@L>#;u<-ne@hwFC;8Gt!?rg#wKw(K>FW6$g2uyIIJ5{E9$5>KjE zMVhA8ET8hXDsoV^zyj)lG*4Rg*?hMXOyzLv_Y3stJ41Igj<`ez2_Zci>Yjb~daC%1 zBN>xfqpp1u9+Rj|DJ?eJ1i7P~XD1&W>+4`-%Pi;Hh!Op@*UEFCs#qqs7QN-JTfXK1=S zHS!EC+#6^GxQPsHKqMKuONx>`Ma`3MYeHEDq=c?sm_?5vJ=(?C3#ObZE~s9+yGyCt zKIYwuriH&tT9NB%%-Y%Io(T@o4a3nP<(uQbqsS(e5G49Wvg8PNhZP(_D=LXtJ4=Y1 zP}3i7ds$xgLS*XBP@eq_@~vaV!_0ylWUIRjb3PE`m`Q!%T#tef6fs#XXARaw_#DVw z5JxYJKserS@f`5tDL%Sm*nomo%){!8bjGSiUC@SWBFCK43Tpi^J(Q)8G`|)qdNbQu zM)9rUououB7_HZ9lMnut?a{o`uJd!tpY*)9`GR$8>r_kqA`h>jjJwK2S% z0%?K8_*-Tymr{QnDrcIB9%3}WAuSaxCT9|ie@y%U;IY6zbF797KpZxJp($vAmzX~# zZjB>2u!_Crvm)%|R)uHxg5`=aXT2jrSO%7JO`pM^rDTdwYSTvdBVh?b9~$Lod6fMh z=Q9m^#yL^a###WZ3P}6SQM_`xxThowe)b9_eP}=dKDr&3K3n5KT8CxVdl*gduw14f z1PEcQC0!yH1{&nU~3k7@uacc?+)!`pK%8$_UDc*|8SDu*r64kpCFNh!YZ zZH@58MNwY}F_krr+694J6V{P9p72QW>4##_N|e$N6G1*XDL_@$DJ+*QI zn2>__xO=_*AV4;#YFnXI=`Qk8BR}n0*cUgmD{OF$5N_x`Y?T>4>UfY3hc7+U+zR zj_i}=t<|nYHjnhqb}nEO=wVluKA#^gkw)){M~1kS<*)~#4PX{yRF1!%Q0SO1l(>bgB?ax?FD6R_rM=G7EV!ImQgU&?zqPtNxn8!Rcz~i|px(#Dnp;Sa zu)GG@_Su~Cgz*gns_(UuZCCSZc5{B&K1nN|M`LZf#2#AJXr=X(;=6(G&HV`v1Rlnv z^#V14K?5J5sWl^sS}VZwA#z+}OP32bzIjBRuefdB4;Mo$4J~scL0ufbn%|1u`~Fsi zCQs*6a@RAotbG(TUC@8vYGWpNqtMKHDG_c1e&iu(QWk}zLNQva4eyfrm||XtQZ9Kn zIO8Eus00x58(7Xa67&eWj-}lwJpiF-P6Y6Z1i4u8;`(|uxrRy-axtZjA|$)plgBZN zU;u4vX@&?obl!ACzLvDDMCQ#IiX_o=RSC-$h7ogdrsgAh#vs!$tuS&N6}RU<5>MF+ z74w@msIvw?+Fn5=seHo1w;0j&rr~(AVD(zkCV-10Q5#Wz738e@eUI$bfbNlx06!Gd zVZgA_@-ZA3!%oD^Cvx!C-3T{tOXd&NR<>IyIQt!6q^zkDzP??~rlW_d1cv7TaB$Q- z0s1|OZcqGwC==R}a4R;Ov%ld?xEH%- zLxkl?bN$X7b+qmiA@GD>Y4*b}*#JsgpfS$&1Q4}2ORDX?nnDI|oQhptKvf}MVXna&3EP`te2z;!csWkUQowqCfBI_5c}@r?I! zF?h9FuGh$8d_v^pW;v!D47K)?ch*}}UM#4S_-FPD3!AqSK|9K%iYRm&VanVIbjG^Z ziwkgcxrx@b32>2fWwBm8+Y*TiRM2rHI7O1`8Jn8)ptYJ?@8`9cqAUpXF3qAj;l-27 zLTFCMjS29a;XD{|lE!K@)`kYfkMPdA%U_E5?5hG*=w>w7#hHj02OcHYyN$~eMHspd zUl)?A?Ywdch2*V|$ol@-kz6HJY78{?`p#wq27(mG3~eUdxk%+mT<182ol4?f9U+^w zS*;V*zZyFyS`mz+XON8OQ8G#1P!`BpJH?MOGI zv%QI&A3=X+6RY`{a)^44yaV2*2%(HsD5Ph5AAqpoyN!{UFE5awF)s&M+ zqWtpZa6Khj42nJ0_tVMe(-Ap?BP4Dq9x&akPe({ALP5u+Ux5wlm0CyyOJCI41V_wJ5+w3BA6W%69d36TB%NKeV1k1B}UAm%qqDwbCyA}=493u3o~BE_P^>t%3n<;5SIRrQtfQAEMy=$~;pPn%VZ2X*> z3RMS@;)aB-DwHsL79}MUkVLO)4k)1sq5a<(x|$*5K`@T-PKX-N`f}82msZfRtCoPj z8^bFvl^)M+{77;2$b>ELQD+QQrs+JcX-K68n?hsBP_ddK*)#R45$_<92Vh4;(-?s7 z9v)|ObTP)0@+p<~ft9J8Vk1zMUwmRduy%lUjr)o3talu4YqQpR$5f_3XuRoYy7^%; zgeT4&I8|syr^3U4e{)|`({qI~LZYypS$Fy4p2ikU4eN=jKUb-B&ll*kh*fb&=Q*n+jD08Asdi61|E_I&*?2P_P)(9HaLqyG{W@$C6xxtTl% zMC|#kDiU=*y1bjt#=koqtB+p1JpJ9x@cJ45oSIZUdp>?x;1cq>S;;pbK3F3cQ^S`q}d%Xl&*Dv&2_n&_kZ~hhh zr;oo?$LrSv_s97AF9z=q@!mcDzgEZV*S`_`{$CE>p9aVOV|BcK{VMoE9e{-6KJ zpDgwL|FJq=zy7c4_`jfD!}}lN^M4Kh>Er*Ax`2K?Qv*oPNMomu|Eu_${+Z%Gx}-k- zSHDA_K6|EL|MR~l@Adz~@&9}H;!m9W|F6E5U+UMtqfV^vUtdSR{-^l)zi^KK%}Y66 zzka2DNcT_w>HC*oJ$v?F;17CaeZIe1$v67-FCNK&>g(&{_4EIi>Uee8XJ7p%@_YUI zO#P6K|99|Tc>I5Z&uC2a`Jd^9qh4u!yZ?mq|M&5wd;C}GIQ{yoU%5wPUesIt`rih} zAN&KkfBmX|r_Z9_>(_q}9RIaCUcZ_P2G`Zc|M%hX|46R?wR-*I-}(3SrTO(M@7=R! z{}Aup=l`4k`@he>(64_f_I8C;x=bZvXkp{>gv+FZe(GLLdM2 z|Ma)`bN%{v{xg1S{`ZynOaA!Z@Zui-_rL!S_zV5I`Ro4m!~6fgg5!U6`@4L+e*Hh* zo0Y|%>-EQP@V|eC|J>{Uvwzs*uk`Cb{hj9P{{?(azxea&#=idNeLnu{|5Ls4JNv({ z?VtSDU&6t3Px|;jQpf+1I^Ms!T+2Phzkwh8WjbDc7#{zd-~{^l_3K~5_y4+c{FmRz zeSi65dDXwu*VOOz>)*ohbP02Pb^ITx(>`J*ggK)S9095U&-s& zvK-ABzA}HwAK&0$I+woxuivWU{$qLlyMMz!;A{Wwv$yK{2f^oGtK(%i*)Qo{=^yu1 z?>+fO{(q~E|EK?hEX==-kN@VsYR6B@Z+v9#-97&7|NRg7?f?Bh{}uORb6x!*eXE~c We@3tW(La96Z$IiARbK{gpZ$L|({6nL From e0c5dc91ecf324913d2c92321e7454c6e3f46cf1 Mon Sep 17 00:00:00 2001 From: J Wyman Date: Mon, 29 Sep 2025 13:59:12 -0400 Subject: [PATCH 082/230] fix: Replace Use of `alloca` (#8411) --- src/data_compressor.h | 12 +++++++----- src/test/data_compressor_test.cc | 22 +++++++++++++--------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/data_compressor.h b/src/data_compressor.h index a63fb43774..c1345e1773 100644 --- a/src/data_compressor.h +++ b/src/data_compressor.h @@ -1,4 +1,4 @@ -// Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved. +// Copyright (c) 2021-2025, NVIDIA CORPORATION. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -93,11 +93,12 @@ class DataCompressor { &stream, deflateEnd); // Get the addr and size of each chunk of memory in 'source' + std::unique_ptr buffer_array_holder; struct evbuffer_iovec* buffer_array = nullptr; int buffer_count = evbuffer_peek(source, -1, NULL, NULL, 0); if (buffer_count > 0) { - buffer_array = static_cast( - alloca(sizeof(struct evbuffer_iovec) * buffer_count)); + buffer_array_holder.reset(new struct evbuffer_iovec[buffer_count]); + buffer_array = buffer_array_holder.get(); if (evbuffer_peek(source, -1, NULL, buffer_array, buffer_count) != buffer_count) { return TRITONSERVER_ErrorNew( @@ -201,11 +202,12 @@ class DataCompressor { &stream, inflateEnd); // Get the addr and size of each chunk of memory in 'source' + std::unique_ptr buffer_array_holder; struct evbuffer_iovec* buffer_array = nullptr; int buffer_count = evbuffer_peek(source, -1, NULL, NULL, 0); if (buffer_count > 0) { - buffer_array = static_cast( - alloca(sizeof(struct evbuffer_iovec) * buffer_count)); + buffer_array_holder.reset(new struct evbuffer_iovec[buffer_count]); + buffer_array = buffer_array_holder.get(); if (evbuffer_peek(source, -1, NULL, buffer_array, buffer_count) != buffer_count) { return TRITONSERVER_ErrorNew( diff --git a/src/test/data_compressor_test.cc b/src/test/data_compressor_test.cc index 292c8c544a..2e68fd8c02 100644 --- a/src/test/data_compressor_test.cc +++ b/src/test/data_compressor_test.cc @@ -1,4 +1,4 @@ -// Copyright 2021-2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -64,11 +64,12 @@ void WriteEVBufferToFile(const std::string& file_name, evbuffer* evb) { std::ofstream fs(file_name); + std::unique_ptr buffer_array_holder; struct evbuffer_iovec* buffer_array = nullptr; int buffer_count = evbuffer_peek(evb, -1, NULL, NULL, 0); if (buffer_count > 0) { - buffer_array = static_cast( - alloca(sizeof(struct evbuffer_iovec) * buffer_count)); + buffer_array_holder.reset(new struct evbuffer_iovec[buffer_count]); + buffer_array = buffer_array_holder.get(); ASSERT_EQ( evbuffer_peek(evb, -1, NULL, buffer_array, buffer_count), buffer_count) << "unexpected error getting buffers for result"; @@ -85,11 +86,12 @@ EVBufferToContiguousBuffer(evbuffer* evb, std::vector* buffer) { *buffer = std::vector(evbuffer_get_length(evb)); { + std::unique_ptr buffer_array_holder; struct evbuffer_iovec* buffer_array = nullptr; int buffer_count = evbuffer_peek(evb, -1, NULL, NULL, 0); if (buffer_count > 0) { - buffer_array = static_cast( - alloca(sizeof(struct evbuffer_iovec) * buffer_count)); + buffer_array_holder.reset(new struct evbuffer_iovec[buffer_count]); + buffer_array = buffer_array_holder.get(); ASSERT_EQ( evbuffer_peek(evb, -1, NULL, buffer_array, buffer_count), buffer_count) @@ -276,11 +278,12 @@ TEST_F(DataCompressorTest, DeflateTwoBuffer) << "Failed to create compressed evbuffer"; // Reconstruct the compressed buffer to be two buffers if (evbuffer_peek(compressed, -1, NULL, NULL, 0) == 1) { + std::unique_ptr buffer_array_holder; struct evbuffer_iovec* buffer_array = nullptr; int buffer_count = evbuffer_peek(compressed, -1, NULL, NULL, 0); if (buffer_count > 0) { - buffer_array = static_cast( - alloca(sizeof(struct evbuffer_iovec) * buffer_count)); + buffer_array_holder.reset(new struct evbuffer_iovec[buffer_count]); + buffer_array = buffer_array_holder.get(); ASSERT_EQ( evbuffer_peek(compressed, -1, NULL, buffer_array, buffer_count), buffer_count) @@ -357,11 +360,12 @@ TEST_F(DataCompressorTest, GzipTwoBuffer) << "Failed to create compressed evbuffer"; // Reconstruct the compressed buffer to be two buffers if (evbuffer_peek(compressed, -1, NULL, NULL, 0) == 1) { + std::unique_ptr buffer_array_holder; struct evbuffer_iovec* buffer_array = nullptr; int buffer_count = evbuffer_peek(compressed, -1, NULL, NULL, 0); if (buffer_count > 0) { - buffer_array = static_cast( - alloca(sizeof(struct evbuffer_iovec) * buffer_count)); + buffer_array_holder.reset(new struct evbuffer_iovec[buffer_count]); + buffer_array = buffer_array_holder.get(); ASSERT_EQ( evbuffer_peek(compressed, -1, NULL, buffer_array, buffer_count), buffer_count) From d2746071897f724f02152efb95affb903071cb98 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Tue, 30 Sep 2025 10:44:32 -0700 Subject: [PATCH 083/230] ci: Update TRT-LLM tests with new config "prompt_embedding_table_data_type" (#8421) --- qa/L0_orca/test.sh | 135 +-------------------------- qa/L0_perf_tensorrt_llm/test.sh | 127 +------------------------ qa/common/trtllm_util.sh | 160 ++++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 260 deletions(-) create mode 100755 qa/common/trtllm_util.sh diff --git a/qa/L0_orca/test.sh b/qa/L0_orca/test.sh index 03f5118bd2..50b7f450fb 100755 --- a/qa/L0_orca/test.sh +++ b/qa/L0_orca/test.sh @@ -46,140 +46,7 @@ SERVER_LOG="${NAME}_server.log" SERVER_TIMEOUT=${SERVER_TIMEOUT:=120} CLIENT_PY=${BASE_DIR}/orca_http_test.py CLIENT_LOG="${NAME}_orca_http_test.log" -source ../common/util.sh - -function clone_tensorrt_llm_backend_repo { - rm -rf $TENSORRTLLM_BACKEND_DIR && mkdir $TENSORRTLLM_BACKEND_DIR - apt-get update && apt-get install git-lfs -y --no-install-recommends - git clone --single-branch --depth=1 -b ${TENSORRTLLM_BACKEND_REPO_TAG} ${TRITON_REPO_ORG}/tensorrtllm_backend.git $TENSORRTLLM_BACKEND_DIR - cd $TENSORRTLLM_BACKEND_DIR && git lfs install && git submodule update --init --recursive -} - -function build_gpt2_base_model { - # Download weights from HuggingFace Transformers - cd ${GPT_DIR} && rm -rf gpt2 && git clone https://huggingface.co/gpt2-medium gpt2 && cd gpt2 - rm pytorch_model.bin model.safetensors - if ! wget -q https://huggingface.co/gpt2-medium/resolve/main/pytorch_model.bin; then - echo "Downloading pytorch_model.bin failed." - exit 1 - fi - cd ${GPT_DIR} - - # Convert weights from HF Tranformers to FT format - python3 convert_checkpoint.py --model_dir gpt2 --dtype float16 --tp_size ${NUM_GPUS} --output_dir "./c-model/gpt2/${NUM_GPUS}-gpu/" - cd ${BASE_DIR} -} - -function build_gpt2_tensorrt_engine { - # Build TensorRT engines - cd ${GPT_DIR} - trtllm-build --checkpoint_dir "./c-model/gpt2/${NUM_GPUS}-gpu/" \ - --gpt_attention_plugin float16 \ - --remove_input_padding enable \ - --paged_kv_cache enable \ - --gemm_plugin float16 \ - --workers "${NUM_GPUS}" \ - --output_dir "${ENGINES_DIR}" - - cd ${BASE_DIR} -} - -function replace_config_tags { - tag_to_replace="${1}" - new_value="${2}" - config_file_path="${3}" - sed -i "s|${tag_to_replace}|${new_value}|g" ${config_file_path} -} - -function prepare_model_repository { - rm -rf ${MODEL_REPOSITORY} && mkdir ${MODEL_REPOSITORY} - cp -r ${TENSORRTLLM_BACKEND_DIR}/tensorrt_llm/triton_backend/all_models/inflight_batcher_llm/* ${MODEL_REPOSITORY} - rm -rf ${MODEL_REPOSITORY}/tensorrt_llm_bls - mv "${MODEL_REPOSITORY}/ensemble" "${MODEL_REPOSITORY}/${MODEL_NAME}" - - replace_config_tags "model_version: -1" "model_version: 1" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" - replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" - replace_config_tags 'name: "ensemble"' "name: \"$MODEL_NAME\"" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" - replace_config_tags '${logits_datatype}' "TYPE_FP32" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" - - replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" - replace_config_tags '${preprocessing_instance_count}' '1' "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" - replace_config_tags '${tokenizer_dir}' "${TOKENIZER_DIR}/" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" - replace_config_tags '${logits_datatype}' "TYPE_FP32" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" - replace_config_tags '${max_queue_delay_microseconds}' "1000000" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" - replace_config_tags '${max_queue_size}' "0" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" - - replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" - replace_config_tags '${postprocessing_instance_count}' '1' "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" - replace_config_tags '${tokenizer_dir}' "${TOKENIZER_DIR}/" "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" - replace_config_tags '${logits_datatype}' "TYPE_FP32" "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" - - replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" - replace_config_tags '${decoupled_mode}' 'true' "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" - replace_config_tags '${max_queue_delay_microseconds}' "1000000" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" - replace_config_tags '${batching_strategy}' 'inflight_fused_batching' "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" - replace_config_tags '${engine_dir}' "${ENGINES_DIR}" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" - replace_config_tags '${triton_backend}' "tensorrtllm" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" - replace_config_tags '${max_queue_size}' "0" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" - replace_config_tags '${logits_datatype}' "TYPE_FP32" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" - replace_config_tags '${encoder_input_features_data_type}' "TYPE_FP32" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" -} - -# Wait until server health endpoint shows ready. Sets WAIT_RET to 0 on -# success, 1 on failure -function wait_for_server_ready() { - local wait_time_secs="${1:-30}" - shift - local spids=("$@") - - WAIT_RET=0 - - for _ in $(seq "$wait_time_secs"); do - for pid in "${spids[@]}"; do - if ! kill -0 "$pid" >/dev/null 2>&1; then - echo "=== Server not running." - WAIT_RET=1 - return - fi - done - - sleep 1 - - if curl -s --fail localhost:8000/v2/health/ready && - curl -s --fail -w "%{http_code}" -o /dev/null -d '{"log_verbose_level":1}' localhost:8000/v2/logging; then - return - fi - done - - echo "=== Timeout $wait_time_secs secs. Server not ready." - WAIT_RET=1 -} - -function run_server { - python3 ${TENSORRTLLM_BACKEND_DIR}/tensorrt_llm/triton_backend/scripts/launch_triton_server.py --world_size="${NUM_GPUS}" --model_repo="${MODEL_REPOSITORY}" >${SERVER_LOG} 2>&1 & - sleep 2 # allow time to obtain the pid(s) - # Read PIDs into an array, trimming whitespaces - readarray -t SERVER_PID < <(pgrep "tritonserver") - - wait_for_server_ready ${SERVER_TIMEOUT} "${SERVER_PID[@]}" - if [ "$WAIT_RET" != "0" ]; then - # Cleanup - kill "${SERVER_PID[@]}" >/dev/null 2>&1 || true - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 - fi -} - -function kill_server { - pgrep tritonserver | xargs kill -SIGINT - for pid in "${SERVER_PID[@]}"; do - echo "Waiting for proc ${pid} to terminate..." - while kill -0 $pid >/dev/null 2>&1; do - sleep 1 - done - done -} +source ../common/trtllm_util.sh clone_tensorrt_llm_backend_repo build_gpt2_base_model diff --git a/qa/L0_perf_tensorrt_llm/test.sh b/qa/L0_perf_tensorrt_llm/test.sh index 5cfb4ba595..5bf418c5c5 100755 --- a/qa/L0_perf_tensorrt_llm/test.sh +++ b/qa/L0_perf_tensorrt_llm/test.sh @@ -43,13 +43,7 @@ SERVER=${TRITON_DIR}/bin/tritonserver BACKEND_DIR=${TRITON_DIR}/backends SERVER_LOG="${NAME}_server.log" SERVER_TIMEOUT=${SERVER_TIMEOUT:=120} - -function clone_tensorrt_llm_backend_repo { - rm -rf $TENSORRTLLM_BACKEND_DIR && mkdir $TENSORRTLLM_BACKEND_DIR - apt-get update && apt-get install git-lfs -y --no-install-recommends - git clone --single-branch --depth=1 -b ${TENSORRTLLM_BACKEND_REPO_TAG} ${TRITON_REPO_ORG}/tensorrtllm_backend.git $TENSORRTLLM_BACKEND_DIR - cd $TENSORRTLLM_BACKEND_DIR && git lfs install && git submodule update --init --recursive -} +source ../common/trtllm_util.sh # Update Open MPI to a version compatible with SLURM. function upgrade_openmpi { @@ -95,125 +89,6 @@ function upgrade_openmpi { mpirun --version } -function build_gpt2_base_model { - # Download weights from HuggingFace Transformers - cd ${GPT_DIR} && rm -rf gpt2 && git clone https://huggingface.co/gpt2-medium gpt2 && cd gpt2 - rm pytorch_model.bin model.safetensors - if ! wget -q https://huggingface.co/gpt2-medium/resolve/main/pytorch_model.bin; then - echo "Downloading pytorch_model.bin failed." - exit 1 - fi - cd ${GPT_DIR} - - # Convert weights from HF Tranformers to FT format - python3 convert_checkpoint.py --model_dir gpt2 --dtype float16 --tp_size ${NUM_GPUS} --output_dir "./c-model/gpt2/${NUM_GPUS}-gpu/" - cd ${BASE_DIR} -} - -function build_gpt2_tensorrt_engine { - # Build TensorRT engines - cd ${GPT_DIR} - trtllm-build --checkpoint_dir "./c-model/gpt2/${NUM_GPUS}-gpu/" \ - --gpt_attention_plugin float16 \ - --remove_input_padding enable \ - --paged_kv_cache enable \ - --gemm_plugin float16 \ - --workers "${NUM_GPUS}" \ - --output_dir "${ENGINES_DIR}" - - cd ${BASE_DIR} -} - -function replace_config_tags { - tag_to_replace="${1}" - new_value="${2}" - config_file_path="${3}" - sed -i "s|${tag_to_replace}|${new_value}|g" ${config_file_path} -} - -function prepare_model_repository { - rm -rf ${MODEL_REPOSITORY} && mkdir ${MODEL_REPOSITORY} - cp -r ${TENSORRTLLM_BACKEND_DIR}/tensorrt_llm/triton_backend/all_models/inflight_batcher_llm/* ${MODEL_REPOSITORY} - rm -rf ${MODEL_REPOSITORY}/tensorrt_llm_bls - mv "${MODEL_REPOSITORY}/ensemble" "${MODEL_REPOSITORY}/${MODEL_NAME}" - - replace_config_tags "model_version: -1" "model_version: 1" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" - replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" - replace_config_tags 'name: "ensemble"' "name: \"$MODEL_NAME\"" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" - - replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" - replace_config_tags '${preprocessing_instance_count}' '1' "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" - replace_config_tags '${tokenizer_dir}' "${TOKENIZER_DIR}/" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" - - replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" - replace_config_tags '${postprocessing_instance_count}' '1' "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" - replace_config_tags '${tokenizer_dir}' "${TOKENIZER_DIR}/" "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" - - replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" - replace_config_tags '${decoupled_mode}' 'true' "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" - replace_config_tags '${max_queue_delay_microseconds}' "1000000" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" - replace_config_tags '${batching_strategy}' 'inflight_fused_batching' "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" - replace_config_tags '${engine_dir}' "${ENGINES_DIR}" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" - replace_config_tags '${triton_backend}' "tensorrtllm" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" - replace_config_tags '${max_queue_size}' "0" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" -} - -# Wait until server health endpoint shows ready. Sets WAIT_RET to 0 on -# success, 1 on failure -function wait_for_server_ready() { - local wait_time_secs="${1:-30}" - shift - local spids=("$@") - - WAIT_RET=0 - - for _ in $(seq "$wait_time_secs"); do - for pid in "${spids[@]}"; do - if ! kill -0 "$pid" >/dev/null 2>&1; then - echo "=== Server not running." - WAIT_RET=1 - return - fi - done - - sleep 1 - - if curl -s --fail localhost:8000/v2/health/ready && - curl -s --fail -w "%{http_code}" -o /dev/null -d '{"log_verbose_level":1}' localhost:8000/v2/logging; then - return - fi - done - - echo "=== Timeout $wait_time_secs secs. Server not ready." - WAIT_RET=1 -} - -function run_server { - python3 ${TENSORRTLLM_BACKEND_DIR}/tensorrt_llm/triton_backend/scripts/launch_triton_server.py --world_size="${NUM_GPUS}" --model_repo="${MODEL_REPOSITORY}" >${SERVER_LOG} 2>&1 & - sleep 2 # allow time to obtain the pid(s) - # Read PIDs into an array, trimming whitespaces - readarray -t SERVER_PID < <(pgrep "tritonserver") - - wait_for_server_ready ${SERVER_TIMEOUT} "${SERVER_PID[@]}" - if [ "$WAIT_RET" != "0" ]; then - # Cleanup - kill "${SERVER_PID[@]}" >/dev/null 2>&1 || true - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 - fi -} - -function kill_server { - pgrep tritonserver | xargs kill -SIGINT - for pid in "${SERVER_PID[@]}"; do - echo "Waiting for proc ${pid} to terminate..." - while kill -0 $pid >/dev/null 2>&1; do - sleep 1 - done - done -} - upgrade_openmpi clone_tensorrt_llm_backend_repo build_gpt2_base_model diff --git a/qa/common/trtllm_util.sh b/qa/common/trtllm_util.sh new file mode 100755 index 0000000000..81ecb2d770 --- /dev/null +++ b/qa/common/trtllm_util.sh @@ -0,0 +1,160 @@ +#!/bin/bash +# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +function clone_tensorrt_llm_backend_repo { + rm -rf $TENSORRTLLM_BACKEND_DIR && mkdir $TENSORRTLLM_BACKEND_DIR + apt-get update && apt-get install git-lfs -y --no-install-recommends + git clone --single-branch --depth=1 -b ${TENSORRTLLM_BACKEND_REPO_TAG} ${TRITON_REPO_ORG}/tensorrtllm_backend.git $TENSORRTLLM_BACKEND_DIR + cd $TENSORRTLLM_BACKEND_DIR && git lfs install && git submodule update --init --recursive +} + +function build_gpt2_base_model { + # Download weights from HuggingFace Transformers + cd ${GPT_DIR} && rm -rf gpt2 && git clone https://huggingface.co/gpt2-medium gpt2 && cd gpt2 + rm pytorch_model.bin model.safetensors + if ! wget -q https://huggingface.co/gpt2-medium/resolve/main/pytorch_model.bin; then + echo "Downloading pytorch_model.bin failed." + exit 1 + fi + cd ${GPT_DIR} + + # Convert weights from HF Tranformers to FT format + python3 convert_checkpoint.py --model_dir gpt2 --dtype float16 --tp_size ${NUM_GPUS} --output_dir "./c-model/gpt2/${NUM_GPUS}-gpu/" + cd ${BASE_DIR} +} + +function build_gpt2_tensorrt_engine { + # Build TensorRT engines + cd ${GPT_DIR} + trtllm-build --checkpoint_dir "./c-model/gpt2/${NUM_GPUS}-gpu/" \ + --gpt_attention_plugin float16 \ + --remove_input_padding enable \ + --paged_kv_cache enable \ + --gemm_plugin float16 \ + --workers "${NUM_GPUS}" \ + --output_dir "${ENGINES_DIR}" + + cd ${BASE_DIR} +} + +function replace_config_tags { + tag_to_replace="${1}" + new_value="${2}" + config_file_path="${3}" + sed -i "s|${tag_to_replace}|${new_value}|g" ${config_file_path} +} + +function prepare_model_repository { + rm -rf ${MODEL_REPOSITORY} && mkdir ${MODEL_REPOSITORY} + cp -r ${TENSORRTLLM_BACKEND_DIR}/tensorrt_llm/triton_backend/all_models/inflight_batcher_llm/* ${MODEL_REPOSITORY} + rm -rf ${MODEL_REPOSITORY}/tensorrt_llm_bls + mv "${MODEL_REPOSITORY}/ensemble" "${MODEL_REPOSITORY}/${MODEL_NAME}" + + replace_config_tags "model_version: -1" "model_version: 1" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" + replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" + replace_config_tags 'name: "ensemble"' "name: \"$MODEL_NAME\"" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" + replace_config_tags '${logits_datatype}' "TYPE_FP32" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" + + replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" + replace_config_tags '${preprocessing_instance_count}' '1' "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" + replace_config_tags '${tokenizer_dir}' "${TOKENIZER_DIR}/" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" + replace_config_tags '${logits_datatype}' "TYPE_FP32" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" + replace_config_tags '${max_queue_delay_microseconds}' "1000000" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" + replace_config_tags '${max_queue_size}' "0" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" + + replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" + replace_config_tags '${postprocessing_instance_count}' '1' "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" + replace_config_tags '${tokenizer_dir}' "${TOKENIZER_DIR}/" "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" + replace_config_tags '${logits_datatype}' "TYPE_FP32" "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" + + replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${decoupled_mode}' 'true' "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${max_queue_delay_microseconds}' "1000000" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${batching_strategy}' 'inflight_fused_batching' "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${engine_dir}' "${ENGINES_DIR}" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${triton_backend}' "tensorrtllm" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${max_queue_size}' "0" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${logits_datatype}' "TYPE_FP32" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${encoder_input_features_data_type}' "TYPE_FP32" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${prompt_embedding_table_data_type}' 'TYPE_FP16' "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" +} + +# Wait until server health endpoint shows ready. Sets WAIT_RET to 0 on +# success, 1 on failure +function wait_for_server_ready() { + local wait_time_secs="${1:-30}" + shift + local spids=("$@") + + WAIT_RET=0 + + for _ in $(seq "$wait_time_secs"); do + for pid in "${spids[@]}"; do + if ! kill -0 "$pid" >/dev/null 2>&1; then + echo "=== Server not running." + WAIT_RET=1 + return + fi + done + + sleep 1 + + if curl -s --fail localhost:8000/v2/health/ready && + curl -s --fail -w "%{http_code}" -o /dev/null -d '{"log_verbose_level":1}' localhost:8000/v2/logging; then + return + fi + done + + echo "=== Timeout $wait_time_secs secs. Server not ready." + WAIT_RET=1 +} + +function run_server { + python3 ${TENSORRTLLM_BACKEND_DIR}/tensorrt_llm/triton_backend/scripts/launch_triton_server.py --world_size="${NUM_GPUS}" --model_repo="${MODEL_REPOSITORY}" >${SERVER_LOG} 2>&1 & + sleep 2 # allow time to obtain the pid(s) + # Read PIDs into an array, trimming whitespaces + readarray -t SERVER_PID < <(pgrep "tritonserver") + + wait_for_server_ready ${SERVER_TIMEOUT} "${SERVER_PID[@]}" + if [ "$WAIT_RET" != "0" ]; then + # Cleanup + kill "${SERVER_PID[@]}" >/dev/null 2>&1 || true + echo -e "\n***\n*** Failed to start $SERVER\n***" + cat $SERVER_LOG + exit 1 + fi +} + +function kill_server { + pgrep tritonserver | xargs kill -SIGINT + for pid in "${SERVER_PID[@]}"; do + echo "Waiting for proc ${pid} to terminate..." + while kill -0 $pid >/dev/null 2>&1; do + sleep 1 + done + done +} From 280a6dc58d339b61be0d1b7a4b97930e7c928f67 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Thu, 2 Oct 2025 08:57:02 -0700 Subject: [PATCH 084/230] test: Fix L0_simple_lib--base (#8431) --- src/simple.cc | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/simple.cc b/src/simple.cc index c429861cc5..cf0cafeed5 100644 --- a/src/simple.cc +++ b/src/simple.cc @@ -1,4 +1,4 @@ -// Copyright 2020-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -876,6 +876,10 @@ main(int argc, char** argv) FAIL_IF_ERR( TRITONSERVER_InferenceResponseDelete(completed_response), "deleting inference response"); + + // We need to make sure that the previous request was released before + // reusing it. + request_release_future.get(); } // The TRITONSERVER_InferenceRequest object can be reused for @@ -906,10 +910,6 @@ main(int argc, char** argv) InferResponseComplete, reinterpret_cast(p)), "setting response callback"); - // We need to make sure that the previous request was released before - // reusing it. - request_release_future.get(); - // Register a new promise for the request callback barrier. barrier = std::make_unique>(); request_release_future = barrier->get_future(); @@ -936,6 +936,10 @@ main(int argc, char** argv) FAIL_IF_ERR( TRITONSERVER_InferenceResponseDelete(completed_response), "deleting inference response"); + + // We need to make sure that the previous request was released before + // reusing it. + request_release_future.get(); } // There are other TRITONSERVER_InferenceRequest APIs that allow @@ -967,7 +971,7 @@ main(int argc, char** argv) // Register a new promise for the request callback barrier. barrier = std::make_unique>(); - request_release_future.get(); + request_release_future = barrier->get_future(); FAIL_IF_ERR( TRITONSERVER_InferenceRequestSetReleaseCallback( @@ -993,6 +997,10 @@ main(int argc, char** argv) FAIL_IF_ERR( TRITONSERVER_InferenceResponseDelete(completed_response), "deleting inference response"); + + // Make sure the request is released before deleting it. If not, release + // callback will segfault after barrier is destructed. + request_release_future.get(); } FAIL_IF_ERR( From 44273e8d07ac4d64bff0a8ce18220b777a55b646 Mon Sep 17 00:00:00 2001 From: J Wyman Date: Fri, 3 Oct 2025 11:32:31 -0400 Subject: [PATCH 085/230] fix: Validate `model_name` when Loading Model from Repository (#8427) Co-authored-by: Yingge He <157551214+yinggeh@users.noreply.github.com> --- .../openai_frontend/engine/utils/triton.py | 14 ++++++- python/openai/tests/test_lora.py | 38 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/python/openai/openai_frontend/engine/utils/triton.py b/python/openai/openai_frontend/engine/utils/triton.py index 3b0f04e3d9..636e58435d 100644 --- a/python/openai/openai_frontend/engine/utils/triton.py +++ b/python/openai/openai_frontend/engine/utils/triton.py @@ -28,6 +28,7 @@ import os import re from dataclasses import asdict, dataclass, field +from pathlib import Path from typing import Iterable, List, Optional, Union import numpy as np @@ -356,13 +357,24 @@ def _get_guided_json_from_tool( def _get_vllm_lora_names( model_repository: str | list[str], model_name: str, model_version: int ) -> None | List[str]: + if ( + len(model_name) == 0 + or model_name.isspace() + or "/" in model_name + or "\\" in model_name + ): + raise ValueError( + f"Invalid model name: '{model_name}'. Model names must be valid file-system-path segment names." + ) lora_names = [] repo_paths = model_repository if isinstance(repo_paths, str): repo_paths = [repo_paths] for repo_path in repo_paths: model_path = os.path.join(repo_path, model_name) - if os.path.normpath(model_path) != model_path: + if (not Path(model_path).is_relative_to(repo_path)) or ( + os.path.normpath(model_path) != model_path + ): raise ValueError( f"Invalid model name: '{model_name}'. Model names must be valid file-system-path segment names." ) diff --git a/python/openai/tests/test_lora.py b/python/openai/tests/test_lora.py index 4a5ff10361..550273cf52 100644 --- a/python/openai/tests/test_lora.py +++ b/python/openai/tests/test_lora.py @@ -29,12 +29,50 @@ import shutil import unittest +import pytest from huggingface_hub import snapshot_download from openai import BadRequestError, NotFoundError +from openai_frontend.engine.utils.triton import ( + _get_vllm_lora_names as get_vllm_lora_names, +) from .utils import OpenAIServer +@pytest.mark.parametrize( + "model_repository,model_name,expect_error", + [ + ("openai_model_repository", "", True), # Empty string as model name. + ("openai_model_repository", " ", True), # Whitespace-only model name. + ("openai_model_repository", "invalid/path", True), + ("openai_model_repository", "invalid\\path", True), + ("openai_model_repository", "../outside/repo", True), + ("openai_model_repository", "../test_models/identity_py", True), + ("test_models", "../test_models/identity_py", True), + ("test_models", "identity_py", False), + ("test_models", "mock_llm", False), + ], +) +def test_get_vllm_lora_name(model_repository: str, model_name: str, expect_error: bool): + try: + get_vllm_lora_names(model_repository, model_name, 1) + except ValueError as e: + if expect_error: + assert ( + f"Invalid model name: '{model_name}'. Model names must be valid file-system-path segment names." + == str(e) + ) + else: + raise pytest.fail( + f"(model_repository='{model_repository}', model_name='{model_name}') raised ValueError unexpectedly: {e}" + ) + else: + if expect_error: + raise pytest.fail( + f"(model_repository='{model_repository}', model_name='{model_name}') did not raise ValueError as expected." + ) + + def is_vllm_installed(): try: import vllm as _ From 8d0b87a575f04278b571cfae1a01be5fcf6175d4 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:46:08 -0700 Subject: [PATCH 086/230] TPRD-1710: Update default branches post-25.09 (#8443) Co-authored-by: Yingge He <157551214+yinggeh@users.noreply.github.com> Co-authored-by: J Wyman Co-authored-by: Dmitry Tokarev --- Dockerfile.sdk | 3 +- README.md | 10 +- build.py | 5 +- deploy/aws/values.yaml | 2 +- deploy/fleetcommand/Chart.yaml | 2 +- deploy/fleetcommand/values.yaml | 6 +- deploy/gcp/values.yaml | 2 +- .../perf-analyzer-script/triton_client.yaml | 2 +- .../server-deployer/build_and_push.sh | 6 +- .../server-deployer/chart/triton/Chart.yaml | 2 +- .../server-deployer/chart/triton/values.yaml | 6 +- .../server-deployer/data-test/schema.yaml | 2 +- .../server-deployer/schema.yaml | 4 +- .../gke-marketplace-app/trt-engine/README.md | 6 +- deploy/k8s-onprem/values.yaml | 2 +- deploy/oci/values.yaml | 2 +- docs/examples/fetch_models.sh | 4 +- docs/introduction/compatibility.md | 5 +- docs/introduction/release_notes.md | 4 +- python/openai/README.md | 2 +- python/openai/requirements.txt | 2 +- .../setup_python_enviroment.sh | 1 + qa/L0_infer/infer_test.py | 683 +++++++++--------- qa/L0_infer/test.sh | 12 +- qa/L0_mlflow/test.sh | 8 +- qa/L0_openai/generate_engine.py | 3 +- qa/common/gen_jetson_trt_models | 4 +- qa/common/gen_qa_custom_ops | 2 +- qa/common/gen_qa_model_repository | 2 +- 29 files changed, 397 insertions(+), 397 deletions(-) diff --git a/Dockerfile.sdk b/Dockerfile.sdk index c73a25b86e..e919b3f50d 100644 --- a/Dockerfile.sdk +++ b/Dockerfile.sdk @@ -29,7 +29,7 @@ # # Base image on the minimum Triton container -ARG BASE_IMAGE=nvcr.io/nvidia/tritonserver:25.08-py3-min +ARG BASE_IMAGE=nvcr.io/nvidia/tritonserver:25.09-py3-min ARG TRITON_CLIENT_REPO_SUBDIR=clientrepo ARG TRITON_REPO_ORGANIZATION=http://github.com/triton-inference-server @@ -167,7 +167,6 @@ RUN apt-get update && \ python3-pip \ python3-setuptools \ python3-wheel \ - software-properties-common \ vim \ wget && \ pip3 install "grpcio<1.68" "grpcio-tools<1.68" diff --git a/README.md b/README.md index 1bf23ead01..7be70a9ecc 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ >[!WARNING] >You are currently on the `main` branch which tracks under-development progress ->towards the next release. The current release is version [2.60.0](https://github.com/triton-inference-server/server/releases/latest) ->and corresponds to the 25.08 container release on NVIDIA GPU Cloud (NGC). +>towards the next release. The current release is version [2.61.0](https://github.com/triton-inference-server/server/releases/latest) +>and corresponds to the 25.09 container release on NVIDIA GPU Cloud (NGC). # Triton Inference Server @@ -90,16 +90,16 @@ Inference Server with the ```bash # Step 1: Create the example model repository -git clone -b r25.08 https://github.com/triton-inference-server/server.git +git clone -b r25.09 https://github.com/triton-inference-server/server.git cd server/docs/examples ./fetch_models.sh # Step 2: Launch triton from the NGC Triton container -docker run --gpus=1 --rm --net=host -v ${PWD}/model_repository:/models nvcr.io/nvidia/tritonserver:25.08-py3 tritonserver --model-repository=/models --model-control-mode explicit --load-model densenet_onnx +docker run --gpus=1 --rm --net=host -v ${PWD}/model_repository:/models nvcr.io/nvidia/tritonserver:25.09-py3 tritonserver --model-repository=/models --model-control-mode explicit --load-model densenet_onnx # Step 3: Sending an Inference Request # In a separate console, launch the image_client example from the NGC Triton SDK container -docker run -it --rm --net=host nvcr.io/nvidia/tritonserver:25.08-py3-sdk /workspace/install/bin/image_client -m densenet_onnx -c 3 -s INCEPTION /workspace/images/mug.jpg +docker run -it --rm --net=host nvcr.io/nvidia/tritonserver:25.09-py3-sdk /workspace/install/bin/image_client -m densenet_onnx -c 3 -s INCEPTION /workspace/images/mug.jpg # Inference should return the following Image '/workspace/images/mug.jpg': diff --git a/build.py b/build.py index 1145a8db9a..f21d5873a8 100755 --- a/build.py +++ b/build.py @@ -73,12 +73,12 @@ DEFAULT_TRITON_VERSION_MAP = { "release_version": "2.62.0dev", "triton_container_version": "25.10dev", - "upstream_container_version": "25.08", + "upstream_container_version": "25.09", "ort_version": "1.23.0", "ort_openvino_version": "2025.3.0", "standalone_openvino_version": "2025.3.0", "dcgm_version": "4.4.0-1", - "vllm_version": "0.9.2", + "vllm_version": "0.10.1.1", "rhel_py_version": "3.12.3", } @@ -1384,7 +1384,6 @@ def dockerfile_prepare_container_linux(argmap, backends, enable_gpu, target_mach libgoogle-perftools-dev \\ libjemalloc-dev \\ libnuma-dev \\ - software-properties-common \\ wget \\ {backend_dependencies} \\ python3-pip \\ diff --git a/deploy/aws/values.yaml b/deploy/aws/values.yaml index 6ec8264ae1..27bec3dd10 100644 --- a/deploy/aws/values.yaml +++ b/deploy/aws/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.08-py3 + imageName: nvcr.io/nvidia/tritonserver:25.09-py3 pullPolicy: IfNotPresent modelRepositoryPath: s3://triton-inference-server-repository/model_repository numGpus: 1 diff --git a/deploy/fleetcommand/Chart.yaml b/deploy/fleetcommand/Chart.yaml index 5aebbe4957..8b0eed9bae 100644 --- a/deploy/fleetcommand/Chart.yaml +++ b/deploy/fleetcommand/Chart.yaml @@ -26,7 +26,7 @@ apiVersion: v1 # appVersion is the Triton version; update when changing release -appVersion: 2.60.0" +appVersion: 2.61.0" description: Triton Inference Server (Fleet Command) name: triton-inference-server # version is the Chart version; update when changing anything in the chart diff --git a/deploy/fleetcommand/values.yaml b/deploy/fleetcommand/values.yaml index 94aa860e77..aa2e45e192 100644 --- a/deploy/fleetcommand/values.yaml +++ b/deploy/fleetcommand/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.08-py3 + imageName: nvcr.io/nvidia/tritonserver:25.09-py3 pullPolicy: IfNotPresent numGpus: 1 serverCommand: tritonserver @@ -47,13 +47,13 @@ image: # # To set model control mode, uncomment and configure below # TODO: Fix the following url, it is invalid - # See https://github.com/triton-inference-server/server/blob/r25.08/docs/user_guide/model_management.md + # See https://github.com/triton-inference-server/server/blob/r25.09/docs/user_guide/model_management.md # for more details #- --model-control-mode=explicit|poll|none # # Additional server args # - # see https://github.com/triton-inference-server/server/blob/r25.08/README.md + # see https://github.com/triton-inference-server/server/blob/r25.09/README.md # for more details service: diff --git a/deploy/gcp/values.yaml b/deploy/gcp/values.yaml index 027cf85819..c4c2ff9951 100644 --- a/deploy/gcp/values.yaml +++ b/deploy/gcp/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.08-py3 + imageName: nvcr.io/nvidia/tritonserver:25.09-py3 pullPolicy: IfNotPresent modelRepositoryPath: gs://triton-inference-server-repository/model_repository numGpus: 1 diff --git a/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml b/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml index 842362d6ce..4b21697c29 100644 --- a/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml +++ b/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml @@ -33,7 +33,7 @@ metadata: namespace: default spec: containers: - - image: nvcr.io/nvidia/tritonserver:25.08-py3-sdk + - image: nvcr.io/nvidia/tritonserver:25.09-py3-sdk imagePullPolicy: Always name: nv-triton-client securityContext: diff --git a/deploy/gke-marketplace-app/server-deployer/build_and_push.sh b/deploy/gke-marketplace-app/server-deployer/build_and_push.sh index 8ff6eeb631..b960b8ff94 100755 --- a/deploy/gke-marketplace-app/server-deployer/build_and_push.sh +++ b/deploy/gke-marketplace-app/server-deployer/build_and_push.sh @@ -27,9 +27,9 @@ export REGISTRY=gcr.io/$(gcloud config get-value project | tr ':' '/') export APP_NAME=tritonserver -export MAJOR_VERSION=2.60 -export MINOR_VERSION=2.60.0 -export NGC_VERSION=25.08-py3 +export MAJOR_VERSION=2.61 +export MINOR_VERSION=2.61.0 +export NGC_VERSION=25.09-py3 docker pull nvcr.io/nvidia/$APP_NAME:$NGC_VERSION diff --git a/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml b/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml index 04d80cbfd4..e86c2b6a21 100644 --- a/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml +++ b/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml @@ -28,4 +28,4 @@ apiVersion: v1 appVersion: "2.60" description: Triton Inference Server name: triton-inference-server -version: 2.60.0 +version: 2.61.0 diff --git a/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml b/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml index 7c7862d3f7..9f87933228 100644 --- a/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml +++ b/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml @@ -31,14 +31,14 @@ maxReplicaCount: 3 tritonProtocol: HTTP # HPA GPU utilization autoscaling target HPATargetAverageValue: 85 -modelRepositoryPath: gs://triton_sample_models/25.08 -publishedVersion: '2.60.0' +modelRepositoryPath: gs://triton_sample_models/25.09 +publishedVersion: '2.61.0' gcpMarketplace: true image: registry: gcr.io repository: nvidia-ngc-public/tritonserver - tag: 25.08-py3 + tag: 25.09-py3 pullPolicy: IfNotPresent # modify the model repository here to match your GCP storage bucket numGpus: 1 diff --git a/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml b/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml index 7a1774155f..107c3ce161 100644 --- a/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml +++ b/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml @@ -27,7 +27,7 @@ x-google-marketplace: schemaVersion: v2 applicationApiVersion: v1beta1 - publishedVersion: '2.60.0' + publishedVersion: '2.61.0' publishedVersionMetadata: releaseNote: >- Initial release. diff --git a/deploy/gke-marketplace-app/server-deployer/schema.yaml b/deploy/gke-marketplace-app/server-deployer/schema.yaml index 607aafe4a5..4bc5fd29e6 100644 --- a/deploy/gke-marketplace-app/server-deployer/schema.yaml +++ b/deploy/gke-marketplace-app/server-deployer/schema.yaml @@ -27,7 +27,7 @@ x-google-marketplace: schemaVersion: v2 applicationApiVersion: v1beta1 - publishedVersion: '2.60.0' + publishedVersion: '2.61.0' publishedVersionMetadata: releaseNote: >- Initial release. @@ -89,7 +89,7 @@ properties: modelRepositoryPath: type: string title: Bucket where models are stored. Please make sure the user/service account to create the GKE app has permission to this GCS bucket. Read Triton documentation on configs and formatting details, supporting TensorRT, TensorFlow, Pytorch, Onnx ... etc. - default: gs://triton_sample_models/25.08 + default: gs://triton_sample_models/25.09 image.ldPreloadPath: type: string title: Leave this empty by default. Triton allows users to create custom layers for backend such as TensorRT plugin, the compiled shared library must be provided via LD_PRELOAD environment variable. diff --git a/deploy/gke-marketplace-app/trt-engine/README.md b/deploy/gke-marketplace-app/trt-engine/README.md index 4651799daf..f2118a1bc3 100644 --- a/deploy/gke-marketplace-app/trt-engine/README.md +++ b/deploy/gke-marketplace-app/trt-engine/README.md @@ -33,7 +33,7 @@ ``` docker run --gpus all -it --network host \ --shm-size=1g --ulimit memlock=-1 --ulimit stack=67108864 \ - -v ~:/scripts nvcr.io/nvidia/tensorrt:25.08-py3 + -v ~:/scripts nvcr.io/nvidia/tensorrt:25.09-py3 pip install onnx six torch tf2onnx tensorflow @@ -57,7 +57,7 @@ mkdir -p engines python3 builder.py -m models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/model.ckpt -o engines/bert_large_int8_bs1_s128.engine -b 1 -s 128 -c models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/ -v models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/vocab.txt --int8 --fp16 --strict --calib-num 1 -iln -imh -gsutil cp bert_large_int8_bs1_s128.engine gs://triton_sample_models/25.08/bert/1/model.plan +gsutil cp bert_large_int8_bs1_s128.engine gs://triton_sample_models/25.09/bert/1/model.plan ``` -For each Triton upgrade, container version used to generate the model, and the model path in GCS `gs://triton_sample_models/25.08/` should be updated accordingly with the correct version. +For each Triton upgrade, container version used to generate the model, and the model path in GCS `gs://triton_sample_models/25.09/` should be updated accordingly with the correct version. diff --git a/deploy/k8s-onprem/values.yaml b/deploy/k8s-onprem/values.yaml index 402a52635f..66f17e7c03 100644 --- a/deploy/k8s-onprem/values.yaml +++ b/deploy/k8s-onprem/values.yaml @@ -30,7 +30,7 @@ tags: openshift: false image: - imageName: nvcr.io/nvidia/tritonserver:25.08-py3 + imageName: nvcr.io/nvidia/tritonserver:25.09-py3 pullPolicy: IfNotPresent modelRepositoryServer: < Replace with the IP Address of your file server > modelRepositoryPath: /srv/models diff --git a/deploy/oci/values.yaml b/deploy/oci/values.yaml index 7261b9c0a3..be4929a0f1 100644 --- a/deploy/oci/values.yaml +++ b/deploy/oci/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:25.08-py3 + imageName: nvcr.io/nvidia/tritonserver:25.09-py3 pullPolicy: IfNotPresent modelRepositoryPath: s3://https://.compat.objectstorage..oraclecloud.com:443/triton-inference-server-repository numGpus: 1 diff --git a/docs/examples/fetch_models.sh b/docs/examples/fetch_models.sh index 7af5a91c1f..ef76833c2b 100755 --- a/docs/examples/fetch_models.sh +++ b/docs/examples/fetch_models.sh @@ -33,9 +33,11 @@ mkdir -p model_repository/inception_onnx/1 wget -O /tmp/inception_v3_2016_08_28_frozen.pb.tar.gz \ https://storage.googleapis.com/download.tensorflow.org/models/inception_v3_2016_08_28_frozen.pb.tar.gz (cd /tmp && tar xzf inception_v3_2016_08_28_frozen.pb.tar.gz) +apt update -qq && apt install -y python3-venv +rm -rf tf2onnx python3 -m venv tf2onnx source ./tf2onnx/bin/activate -pip3 install "numpy<2" tensorflow tf2onnx +pip3 install "numpy<2" tensorflow==2.18.1 tf2onnx==1.16.1 onnx==1.16.1 python3 -m tf2onnx.convert --graphdef /tmp/inception_v3_2016_08_28_frozen.pb --output inception_v3_onnx.model.onnx --inputs input:0 --outputs InceptionV3/Predictions/Softmax:0 deactivate mv inception_v3_onnx.model.onnx model_repository/inception_onnx/1/model.onnx diff --git a/docs/introduction/compatibility.md b/docs/introduction/compatibility.md index 9ada3d122e..1e1dbb5e3c 100644 --- a/docs/introduction/compatibility.md +++ b/docs/introduction/compatibility.md @@ -38,6 +38,7 @@ | Triton release version | NGC Tag | Python version | Torch version | TensorRT version | TensorRT-LLM version | CUDA version | CUDA Driver version | Size | | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 25.09 | nvcr.io/nvidia/tritonserver:25.09-trtllm-python-py3 | Python 3.12.3 | 2.8.0a0+5228986c39.nv25.6 | 10.11.0.33 | 1.0.0 | 12.9.1.010 | 575.57.08 | 16.25 GB | | 25.08 | nvcr.io/nvidia/tritonserver:25.08-trtllm-python-py3 | Python 3.12.3 | 2.8.0a0+5228986c39.nv25.5 | 10.11.0.33 | 0.21.0 | 12.9.0.043 | 575.51.03 | 20.49 GB | | 25.07 | nvcr.io/nvidia/tritonserver:25.07-trtllm-python-py3 | Python 3.12.3 | 2.7.0a0+79aa17489c.nv25.4 | 10.10.0.31 | 0.20.0 | 12.9.0.036 | 575.51.03 | 18.3G | | 25.06 | nvcr.io/nvidia/tritonserver:25.06-trtllm-python-py3 | Python 3.12.3 | 2.7.0a0+79aa17489c.nv25.4 | 10.10.0.31 | 0.20.0 | 12.9.0.036 | 575.51.03 | 18.3G | @@ -60,7 +61,8 @@ | Triton release version | NGC Tag | Python version | vLLM version | CUDA version | CUDA Driver version | Size | | --- | --- | --- | --- | --- | --- | --- | -| 25.08 | nvcr.io/nvidia/tritonserver:25.08-vllm-python-py3 | Python 3.12.3 | 0.9.2+4ef1e343.nv25.8.post1.cu130 | 12.9.0.043 | 575.51.03 | 8.1G | +| 25.09 | nvcr.io/nvidia/tritonserver:25.09-vllm-python-py3 | Python 3.12.3 | 0.10.1.1+381074ae.nv25.9.cu130 | 13.0.1.012 | 580.82.07 | 7.78G | +| 25.08 | nvcr.io/nvidia/tritonserver:25.08-vllm-python-py3 | Python 3.12.3 | 0.9.2+4ef1e343.nv25.8.post1.cu130 | 13.0.1.012 | 580.82.07 | 8.1G | | 25.07 | nvcr.io/nvidia/tritonserver:25.07-vllm-python-py3 | Python 3.12.3 | 0.9.0rc1+1958ee56.nv25.6.cu129 | 12.9.0.043 | 575.51.03 | 10G | | 25.06 | nvcr.io/nvidia/tritonserver:25.06-vllm-python-py3 | Python 3.12.3 | 0.9.0rc1+1958ee56.nv25.6.cu129 | 12.9.0.043 | 575.51.03 | 10G | | 25.05 | nvcr.io/nvidia/tritonserver:25.05-vllm-python-py3 | Python 3.12.3 | 0.8.4+dc1a3e10.nv25.5.cu129 | 12.9.0.043 | 575.51.03 | 10G | @@ -82,6 +84,7 @@ | Triton release version | ONNX Runtime | | --- | --- | +| 25.09 | 1.23.0 | | 25.08 | 1.23.0+1d1712fdaf | | 25.07 | 1.22.0 | | 25.06 | 1.22.0 | diff --git a/docs/introduction/release_notes.md b/docs/introduction/release_notes.md index 7038abbb01..31fa2784c9 100644 --- a/docs/introduction/release_notes.md +++ b/docs/introduction/release_notes.md @@ -25,9 +25,9 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --> -# [Triton Inference Server Release 25.06](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-25-06.html#rel-25-06) +# [Triton Inference Server Release 25.09](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-25-09.html#rel-25-09) -The Triton Inference Server container image, release 25.06, is available +The Triton Inference Server container image, release 25.09, is available on [NGC](https://ngc.nvidia.com/catalog/containers/nvidia:tritonserver) and is open source on [GitHub](https://github.com/triton-inference-server/server). Release notes can diff --git a/python/openai/README.md b/python/openai/README.md index f5ea906a86..5dda869bab 100644 --- a/python/openai/README.md +++ b/python/openai/README.md @@ -51,7 +51,7 @@ docker run -it --net=host --gpus all --rm \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ -e HF_TOKEN \ - nvcr.io/nvidia/tritonserver:25.08-vllm-python-py3 + nvcr.io/nvidia/tritonserver:25.09-vllm-python-py3 ``` 2. Launch the OpenAI-compatible Triton Inference Server: diff --git a/python/openai/requirements.txt b/python/openai/requirements.txt index 5536880c09..608f3b5ac3 100644 --- a/python/openai/requirements.txt +++ b/python/openai/requirements.txt @@ -29,7 +29,7 @@ fastapi==0.115.6 # Fix httpx version to avoid bug in openai library: # https://community.openai.com/t/error-with-openai-1-56-0-client-init-got-an-unexpected-keyword-argument-proxies/1040332/3 httpx==0.27.2 -openai==1.60.0 +openai==1.107.3 partial-json-parser # used for parsing partial JSON outputs # Minimum starlette version needed to address CVE: # https://github.com/advisories/GHSA-f96h-pmfr-66vw diff --git a/qa/L0_backend_python/setup_python_enviroment.sh b/qa/L0_backend_python/setup_python_enviroment.sh index cfa56cba51..97901e1bb6 100755 --- a/qa/L0_backend_python/setup_python_enviroment.sh +++ b/qa/L0_backend_python/setup_python_enviroment.sh @@ -99,6 +99,7 @@ echo "python environment 3.${PYTHON_ENV_VERSION}" # copy the stub out to /opt/tritonserver/backends/python/triton_python_backend_stub cp python_backend/builddir/triton_python_backend_stub /opt/tritonserver/backends/python/triton_python_backend_stub # Set up environment and stub for each test +apt-get update -qq && apt-get install -y software-properties-common add-apt-repository ppa:deadsnakes/ppa -y apt-get update && apt-get -y install \ "python3.${PYTHON_ENV_VERSION}-dev" \ diff --git a/qa/L0_infer/infer_test.py b/qa/L0_infer/infer_test.py index 8f16a7fe10..3d01160b2e 100755 --- a/qa/L0_infer/infer_test.py +++ b/qa/L0_infer/infer_test.py @@ -42,6 +42,7 @@ TEST_CUDA_SHARED_MEMORY = bool(int(os.environ.get("TEST_CUDA_SHARED_MEMORY", 0))) CPU_ONLY = os.environ.get("TRITON_SERVER_CPU_ONLY") is not None TEST_VALGRIND = bool(int(os.environ.get("TEST_VALGRIND", 0))) +VALGRIND_TESTS = bool(int(os.environ.get("VALGRIND_TESTS", 0))) USE_GRPC = os.environ.get("USE_GRPC", 1) != "0" USE_HTTP = os.environ.get("USE_HTTP", 1) != "0" @@ -69,7 +70,6 @@ def _full_exact( output0_raw, output1_raw, swap, - network_timeout=NETWORK_TIMEOUT, ): def _infer_exact_helper( tester, @@ -163,31 +163,6 @@ def _infer_exact_helper( ): ensemble_prefix.append(prefix) - if tu.validate_for_onnx_model( - input_dtype, - output0_dtype, - output1_dtype, - (input_size,), - (input_size,), - (input_size,), - ): - for prefix in ensemble_prefix: - for pf in ["onnx"]: - if pf in BACKENDS: - _infer_exact_helper( - self, - prefix + pf, - (input_size,), - 8, - input_dtype, - output0_dtype, - output1_dtype, - output0_raw=output0_raw, - output1_raw=output1_raw, - swap=swap, - network_timeout=network_timeout, - ) - if not CPU_ONLY and tu.validate_for_trt_model( input_dtype, output0_dtype, @@ -666,16 +641,59 @@ def test_mix_iff(self): swap=False, ) - def test_raw_version_latest_1(self): - input_size = 16 - tensor_shape = (1, input_size) + if not VALGRIND_TESTS: + + def test_raw_version_latest_1(self): + input_size = 16 + tensor_shape = (1, input_size) + + # There are 3 versions of onnx_int8_int8_int8 but + # only version 3 should be available + for platform in ["onnx"]: + if platform not in BACKENDS: + continue + try: + iu.infer_exact( + self, + platform, + tensor_shape, + 1, + np.int8, + np.int8, + np.int8, + model_version=1, + swap=False, + use_http=USE_HTTP, + use_grpc=USE_GRPC, + use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, + use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, + ) + except InferenceServerException as ex: + self.assertTrue( + ex.message().startswith("Request for unknown model") + ) + + try: + iu.infer_exact( + self, + platform, + tensor_shape, + 1, + np.int8, + np.int8, + np.int8, + model_version=2, + swap=True, + use_http=USE_HTTP, + use_grpc=USE_GRPC, + use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, + use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, + ) + except InferenceServerException as ex: + self.assertTrue( + ex.message().startswith("Request for unknown model") + ) - # There are 3 versions of onnx_int8_int8_int8 but - # only version 3 should be available - for platform in ["onnx"]: - if platform not in BACKENDS: - continue - try: iu.infer_exact( self, platform, @@ -684,25 +702,52 @@ def test_raw_version_latest_1(self): np.int8, np.int8, np.int8, - model_version=1, - swap=False, + model_version=3, + swap=True, use_http=USE_HTTP, use_grpc=USE_GRPC, use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) - except InferenceServerException as ex: - self.assertTrue(ex.message().startswith("Request for unknown model")) - try: + def test_raw_version_latest_2(self): + input_size = 16 + tensor_shape = (1, input_size) + + # There are 3 versions of onnx_int16_int16_int16 but only + # versions 2 and 3 should be available + for platform in ["onnx"]: + if platform not in BACKENDS: + continue + try: + iu.infer_exact( + self, + platform, + tensor_shape, + 1, + np.int16, + np.int16, + np.int16, + model_version=1, + swap=False, + use_http=USE_HTTP, + use_grpc=USE_GRPC, + use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, + use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, + ) + except InferenceServerException as ex: + self.assertTrue( + ex.message().startswith("Request for unknown model") + ) + iu.infer_exact( self, platform, tensor_shape, 1, - np.int8, - np.int8, - np.int8, + np.int16, + np.int16, + np.int16, model_version=2, swap=True, use_http=USE_HTTP, @@ -710,35 +755,6 @@ def test_raw_version_latest_1(self): use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) - except InferenceServerException as ex: - self.assertTrue(ex.message().startswith("Request for unknown model")) - - iu.infer_exact( - self, - platform, - tensor_shape, - 1, - np.int8, - np.int8, - np.int8, - model_version=3, - swap=True, - use_http=USE_HTTP, - use_grpc=USE_GRPC, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) - - def test_raw_version_latest_2(self): - input_size = 16 - tensor_shape = (1, input_size) - - # There are 3 versions of onnx_int16_int16_int16 but only - # versions 2 and 3 should be available - for platform in ["onnx"]: - if platform not in BACKENDS: - continue - try: iu.infer_exact( self, platform, @@ -747,136 +763,46 @@ def test_raw_version_latest_2(self): np.int16, np.int16, np.int16, - model_version=1, - swap=False, + model_version=3, + swap=True, use_http=USE_HTTP, use_grpc=USE_GRPC, use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) - except InferenceServerException as ex: - self.assertTrue(ex.message().startswith("Request for unknown model")) - - iu.infer_exact( - self, - platform, - tensor_shape, - 1, - np.int16, - np.int16, - np.int16, - model_version=2, - swap=True, - use_http=USE_HTTP, - use_grpc=USE_GRPC, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) - iu.infer_exact( - self, - platform, - tensor_shape, - 1, - np.int16, - np.int16, - np.int16, - model_version=3, - swap=True, - use_http=USE_HTTP, - use_grpc=USE_GRPC, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) - - def test_raw_version_all(self): - input_size = 16 - tensor_shape = (1, input_size) - # There are 3 versions of *_int32_int32_int32 and all should - # be available. - for platform in ["onnx"]: - if platform not in BACKENDS: - continue - iu.infer_exact( - self, - platform, - tensor_shape, - 1, - np.int32, - np.int32, - np.int32, - model_version=1, - swap=False, - use_http=USE_HTTP, - use_grpc=USE_GRPC, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) - iu.infer_exact( - self, - platform, - tensor_shape, - 1, - np.int32, - np.int32, - np.int32, - model_version=2, - swap=True, - use_http=USE_HTTP, - use_grpc=USE_GRPC, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) - iu.infer_exact( - self, - platform, - tensor_shape, - 1, - np.int32, - np.int32, - np.int32, - model_version=3, - swap=True, - use_http=USE_HTTP, - use_grpc=USE_GRPC, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) - - def test_raw_version_specific_1(self): - input_size = 16 - tensor_shape = (1, input_size) - - # There are 3 versions of *_float16_float16_float16 but only - # version 1 should be available. - for platform in ["onnx"]: - if platform not in BACKENDS: - continue - iu.infer_exact( - self, - platform, - tensor_shape, - 1, - np.float16, - np.float16, - np.float16, - model_version=1, - swap=False, - use_http=USE_HTTP, - use_grpc=USE_GRPC, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) + def test_raw_version_all(self): + input_size = 16 + tensor_shape = (1, input_size) - try: + # There are 3 versions of onnx_int32_int32_int32 and all should + # be available. + for platform in ["onnx"]: + if platform not in BACKENDS: + continue iu.infer_exact( self, platform, tensor_shape, 1, - np.float16, - np.float16, - np.float16, + np.int32, + np.int32, + np.int32, + model_version=1, + swap=False, + use_http=USE_HTTP, + use_grpc=USE_GRPC, + use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, + use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, + ) + iu.infer_exact( + self, + platform, + tensor_shape, + 1, + np.int32, + np.int32, + np.int32, model_version=2, swap=True, use_http=USE_HTTP, @@ -884,18 +810,14 @@ def test_raw_version_specific_1(self): use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) - except InferenceServerException as ex: - self.assertTrue(ex.message().startswith("Request for unknown model")) - - try: iu.infer_exact( self, platform, tensor_shape, 1, - np.float16, - np.float16, - np.float16, + np.int32, + np.int32, + np.int32, model_version=3, swap=True, use_http=USE_HTTP, @@ -903,143 +825,149 @@ def test_raw_version_specific_1(self): use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) - except InferenceServerException as ex: - self.assertTrue(ex.message().startswith("Request for unknown model")) - def test_raw_version_specific_1_3(self): - input_size = 16 - - # There are 3 versions of *_float32_float32_float32 but only - # versions 1 and 3 should be available. - for platform in ("onnx", "plan"): - if platform == "plan" and CPU_ONLY: - continue - if platform not in BACKENDS: - continue + def test_raw_version_specific_1(self): + input_size = 16 tensor_shape = (1, input_size) - iu.infer_exact( - self, - platform, - tensor_shape, - 1, - np.float32, - np.float32, - np.float32, - model_version=1, - swap=False, - use_http=USE_HTTP, - use_grpc=USE_GRPC, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) - try: + # There are 3 versions of onnx_float16_float16_float16 but only + # version 1 should be available. + for platform in ["onnx"]: + if platform not in BACKENDS: + continue iu.infer_exact( self, platform, tensor_shape, 1, - np.float32, - np.float32, - np.float32, - model_version=2, - swap=True, + np.float16, + np.float16, + np.float16, + model_version=1, + swap=False, use_http=USE_HTTP, use_grpc=USE_GRPC, use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) - except InferenceServerException as ex: - self.assertTrue(ex.message().startswith("Request for unknown model")) - - iu.infer_exact( - self, - platform, - tensor_shape, - 1, - np.float32, - np.float32, - np.float32, - model_version=3, - swap=True, - use_http=USE_HTTP, - use_grpc=USE_GRPC, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) - if ENSEMBLES: - if all(x in BACKENDS for x in ["onnx", "plan"]): - - def test_ensemble_mix_platform(self): - # Skip on CPU only machine as TensorRT model is used in this ensemble - if CPU_ONLY: - return - for bs in (1, 8): + try: iu.infer_exact( self, - "mix_platform", - (bs, 16), - bs, - np.float32, - np.float32, - np.float32, + platform, + tensor_shape, + 1, + np.float16, + np.float16, + np.float16, + model_version=2, + swap=True, use_http=USE_HTTP, use_grpc=USE_GRPC, use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) + except InferenceServerException as ex: + self.assertTrue( + ex.message().startswith("Request for unknown model") + ) - if "onnx" in BACKENDS: - - def test_ensemble_mix_type(self): - for bs in (1, 8): + try: iu.infer_exact( self, - "mix_type", - (bs, 16), - bs, - np.int32, - np.float32, - np.float32, + platform, + tensor_shape, + 1, + np.float16, + np.float16, + np.float16, + model_version=3, + swap=True, use_http=USE_HTTP, use_grpc=USE_GRPC, use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) + except InferenceServerException as ex: + self.assertTrue( + ex.message().startswith("Request for unknown model") + ) - if all(x in BACKENDS for x in ["onnx", "plan"]): + def test_raw_version_specific_1_3(self): + input_size = 16 + + # There are 3 versions of *_float32_float32_float32 but only + # versions 1 and 3 should be available. + for platform in ("onnx", "plan"): + if platform == "plan" and CPU_ONLY: + continue + if platform not in BACKENDS: + continue + tensor_shape = (1, input_size) + iu.infer_exact( + self, + platform, + tensor_shape, + 1, + np.float32, + np.float32, + np.float32, + model_version=1, + swap=False, + use_http=USE_HTTP, + use_grpc=USE_GRPC, + use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, + use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, + ) - def test_ensemble_mix_ensemble(self): - for bs in (1, 8): + try: iu.infer_exact( self, - "mix_ensemble", - (bs, 16), - bs, - np.int32, + platform, + tensor_shape, + 1, + np.float32, np.float32, np.float32, + model_version=2, + swap=True, use_http=USE_HTTP, use_grpc=USE_GRPC, use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) + except InferenceServerException as ex: + self.assertTrue( + ex.message().startswith("Request for unknown model") + ) - if all( - x in BACKENDS - for x in [ - "onnx", - ] - ): + iu.infer_exact( + self, + platform, + tensor_shape, + 1, + np.float32, + np.float32, + np.float32, + model_version=3, + swap=True, + use_http=USE_HTTP, + use_grpc=USE_GRPC, + use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, + use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, + ) + + if ENSEMBLES: + if all(x in BACKENDS for x in ["onnx", "libtorch"]): - def test_ensemble_mix_batch_nobatch(self): - base_names = ["batch_to_nobatch", "nobatch_to_batch"] - for name in base_names: + def test_ensemble_mix_platform(self): + # Skip on CPU only machine as TensorRT model is used in this ensemble + if CPU_ONLY: + return for bs in (1, 8): iu.infer_exact( self, - name, + "mix_platform", (bs, 16), bs, np.float32, @@ -1050,60 +978,28 @@ def test_ensemble_mix_batch_nobatch(self): use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) - iu.infer_exact( - self, - name + "_nobatch", - (8, 16), - 1, - np.float32, - np.float32, - np.float32, - use_http=USE_HTTP, - use_grpc=USE_GRPC, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) - - # batch -> nobatch -> batch - for bs in (1, 8): - iu.infer_exact( - self, - "mix_nobatch_batch", - (bs, 16), - bs, - np.float32, - np.float32, - np.float32, - use_http=USE_HTTP, - use_grpc=USE_GRPC, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) - if not (TEST_SYSTEM_SHARED_MEMORY or TEST_CUDA_SHARED_MEMORY): + if "onnx" in BACKENDS: - def test_ensemble_label_lookup(self): - if all(x in BACKENDS for x in ["onnx", "plan"]): - # Ensemble needs to look up label from the actual model + def test_ensemble_mix_type(self): for bs in (1, 8): iu.infer_exact( self, - "mix_platform", + "mix_type", (bs, 16), bs, + np.int32, np.float32, np.float32, - np.float32, - output0_raw=False, - output1_raw=False, use_http=USE_HTTP, use_grpc=USE_GRPC, use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) - if all(x in BACKENDS for x in ["onnx", "plan"]): - # Label from the actual model will be passed along the nested ensemble + if all(x in BACKENDS for x in ["onnx", "libtorch"]): + + def test_ensemble_mix_ensemble(self): for bs in (1, 8): iu.infer_exact( self, @@ -1113,55 +1009,148 @@ def test_ensemble_label_lookup(self): np.int32, np.float32, np.float32, - output0_raw=False, - output1_raw=False, use_http=USE_HTTP, use_grpc=USE_GRPC, use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) - if "onnx" in BACKENDS: - # If label file is provided, it will use the provided label file directly - try: + if all( + x in BACKENDS + for x in [ + "onnx", + ] + ): + + def test_ensemble_mix_batch_nobatch(self): + base_names = ["batch_to_nobatch", "nobatch_to_batch"] + for name in base_names: + for bs in (1, 8): + iu.infer_exact( + self, + name, + (bs, 16), + bs, + np.float32, + np.float32, + np.float32, + use_http=USE_HTTP, + use_grpc=USE_GRPC, + use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, + use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, + ) iu.infer_exact( self, - "wrong_label", - (1, 16), + name + "_nobatch", + (8, 16), 1, - np.int32, np.float32, np.float32, - output0_raw=False, - output1_raw=False, + np.float32, use_http=USE_HTTP, use_grpc=USE_GRPC, use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) - except AssertionError: - # Sanity check that infer_exact failed since this ensemble is provided - # with unexpected labels - pass - if "onnx" in BACKENDS: + # batch -> nobatch -> batch for bs in (1, 8): iu.infer_exact( self, - "label_override", + "mix_nobatch_batch", (bs, 16), bs, - np.int32, np.float32, np.float32, - output0_raw=False, - output1_raw=False, + np.float32, use_http=USE_HTTP, use_grpc=USE_GRPC, use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) + if not (TEST_SYSTEM_SHARED_MEMORY or TEST_CUDA_SHARED_MEMORY): + + def test_ensemble_label_lookup(self): + if all(x in BACKENDS for x in ["onnx", "libtorch"]): + # Ensemble needs to look up label from the actual model + for bs in (1, 8): + iu.infer_exact( + self, + "mix_platform", + (bs, 16), + bs, + np.float32, + np.float32, + np.float32, + output0_raw=False, + output1_raw=False, + use_http=USE_HTTP, + use_grpc=USE_GRPC, + use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, + use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, + ) + + if all(x in BACKENDS for x in ["onnx", "libtorch"]): + # Label from the actual model will be passed along the nested ensemble + for bs in (1, 8): + iu.infer_exact( + self, + "mix_ensemble", + (bs, 16), + bs, + np.int32, + np.float32, + np.float32, + output0_raw=False, + output1_raw=False, + use_http=USE_HTTP, + use_grpc=USE_GRPC, + use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, + use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, + ) + + if "onnx" in BACKENDS: + # If label file is provided, it will use the provided label file directly + try: + iu.infer_exact( + self, + "wrong_label", + (1, 16), + 1, + np.int32, + np.float32, + np.float32, + output0_raw=False, + output1_raw=False, + use_http=USE_HTTP, + use_grpc=USE_GRPC, + use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, + use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, + ) + except AssertionError: + # Sanity check that infer_exact failed since this ensemble is provided + # with unexpected labels + pass + + if "onnx" in BACKENDS: + for bs in (1, 8): + iu.infer_exact( + self, + "label_override", + (bs, 16), + bs, + np.int32, + np.float32, + np.float32, + output0_raw=False, + output1_raw=False, + use_http=USE_HTTP, + use_grpc=USE_GRPC, + use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, + use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, + ) + if __name__ == "__main__": unittest.main() diff --git a/qa/L0_infer/test.sh b/qa/L0_infer/test.sh index 40625ae6bd..72df50bd41 100755 --- a/qa/L0_infer/test.sh +++ b/qa/L0_infer/test.sh @@ -349,7 +349,7 @@ done # separately to reduce the loading time. if [ "$TEST_VALGRIND" -eq 1 ]; then TESTING_BACKENDS="python python_dlpack onnx" - EXPECTED_NUM_TESTS=42 + EXPECTED_NUM_TESTS=36 if [[ "aarch64" != $(uname -m) ]] ; then pip3 install torch==2.3.1+cpu -f https://download.pytorch.org/whl/torch_stable.html else @@ -364,9 +364,11 @@ if [ "$TEST_VALGRIND" -eq 1 ]; then mkdir nobatch_models mv ./models/*nobatch_* ./nobatch_models/. cp -fr ./models/nop_* ./nobatch_models/. - # These two models are required by test_ensemble_mix_batch_nobatch test case. - cp -fr ./models/onnx_float32_float32_float32 ./nobatch_models/. - cp -fr ./models/custom_zero_1_float32 ./nobatch_models/. + if [[ $BACKENDS == *"onnx"* ]]; then + # These two models are required by test_ensemble_mix_batch_nobatch test case. + cp -fr ./models/onnx_float32_float32_float32 ./nobatch_models/. + cp -fr ./models/custom_zero_1_float32 ./nobatch_models/. + fi for BATCHING_MODE in batch nobatch; do if [ "$TRITON_SERVER_CPU_ONLY" == "1" ]; then @@ -406,7 +408,7 @@ if [ "$TEST_VALGRIND" -eq 1 ]; then set +e - python3 $INFER_TEST >$CLIENT_LOG 2>&1 + VALGRIND_TESTS="1" python3 $INFER_TEST >$CLIENT_LOG 2>&1 if [ $? -ne 0 ]; then cat $CLIENT_LOG RET=1 diff --git a/qa/L0_mlflow/test.sh b/qa/L0_mlflow/test.sh index 4b5205ba25..1bf7bc18f8 100755 --- a/qa/L0_mlflow/test.sh +++ b/qa/L0_mlflow/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2022-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -39,7 +39,11 @@ rm -fr *.log *.json # install a higher version of python which uses blinker 1.6, # but it is unknown whether this test should rely on # the default installation of python. -apt remove -y python3-blinker + +apt update -qq && apt install python3-venv -y +python3 -m venv .venv + +source .venv/bin/activate RET=0 diff --git a/qa/L0_openai/generate_engine.py b/qa/L0_openai/generate_engine.py index b71e084b03..83ea35a88d 100644 --- a/qa/L0_openai/generate_engine.py +++ b/qa/L0_openai/generate_engine.py @@ -25,7 +25,8 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from argparse import ArgumentParser -from tensorrt_llm import LLM, BuildConfig +from tensorrt_llm import BuildConfig +from tensorrt_llm._tensorrt_engine import LLM from tensorrt_llm.plugin import PluginConfig diff --git a/qa/common/gen_jetson_trt_models b/qa/common/gen_jetson_trt_models index bf3281bc9b..9b652a6977 100755 --- a/qa/common/gen_jetson_trt_models +++ b/qa/common/gen_jetson_trt_models @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2020-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -34,7 +34,7 @@ # Make all generated files accessible outside of container umask 0000 # Set the version of the models -TRITON_VERSION=${TRITON_VERSION:=25.08} +TRITON_VERSION=${TRITON_VERSION:=25.09} # Set the CUDA device to use CUDA_DEVICE=${RUNNER_ID:=0} # Set TensorRT image diff --git a/qa/common/gen_qa_custom_ops b/qa/common/gen_qa_custom_ops index a716c0e517..1d7f6db05a 100755 --- a/qa/common/gen_qa_custom_ops +++ b/qa/common/gen_qa_custom_ops @@ -37,7 +37,7 @@ ## ############################################################################ -TRITON_VERSION=${TRITON_VERSION:=25.08} +TRITON_VERSION=${TRITON_VERSION:=25.09} NVIDIA_UPSTREAM_VERSION=${NVIDIA_UPSTREAM_VERSION:=$TRITON_VERSION} PYTORCH_IMAGE=${PYTORCH_IMAGE:=nvcr.io/nvidia/pytorch:$NVIDIA_UPSTREAM_VERSION-py3} UBUNTU_IMAGE=${UBUNTU_IMAGE:=ubuntu:24.04} diff --git a/qa/common/gen_qa_model_repository b/qa/common/gen_qa_model_repository index 67e2adb902..14d6b85011 100755 --- a/qa/common/gen_qa_model_repository +++ b/qa/common/gen_qa_model_repository @@ -48,7 +48,7 @@ ## ############################################################################ -TRITON_VERSION=${TRITON_VERSION:=25.08} +TRITON_VERSION=${TRITON_VERSION:=25.09} # ONNX. Use ONNX_OPSET 0 to use the default for ONNX version ONNX_VERSION=1.16.1 From d5515db589850e627900586869c65e8bc75826ab Mon Sep 17 00:00:00 2001 From: J Wyman Date: Thu, 9 Oct 2025 11:42:08 -0400 Subject: [PATCH 087/230] fix: Handle Parsing Error (#8447) Signed-off-by: J Wyman --- src/http_server.cc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/http_server.cc b/src/http_server.cc index f62f73c948..3c014d8e7d 100644 --- a/src/http_server.cc +++ b/src/http_server.cc @@ -2361,6 +2361,16 @@ HTTPAPIServer::GetContentLength( ", got: " + content_length_c_str) .c_str()); } + catch (const std::out_of_range& oor) { + err = TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + (std::string("Unable to parse ") + kContentLengthHeader + + ", value is out of range [ " + + std::to_string(std::numeric_limits::min()) + ", " + + std::to_string(std::numeric_limits::max()) + + " ], got: " + content_length_c_str) + .c_str()); + } } } else { // The Content-Length doesn't reflect the actual request body size From c31c40f4d852a8f47a75e85ebfb66c01ed468082 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Thu, 9 Oct 2025 08:52:19 -0700 Subject: [PATCH 088/230] Update ONNX Runtime version to 1.23.1 (#8446) --- build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.py b/build.py index f21d5873a8..4044aa599d 100755 --- a/build.py +++ b/build.py @@ -74,7 +74,7 @@ "release_version": "2.62.0dev", "triton_container_version": "25.10dev", "upstream_container_version": "25.09", - "ort_version": "1.23.0", + "ort_version": "1.23.1", "ort_openvino_version": "2025.3.0", "standalone_openvino_version": "2025.3.0", "dcgm_version": "4.4.0-1", From be7d4b1a1eb06c53bcef27d506cf1104ff7e2e97 Mon Sep 17 00:00:00 2001 From: Sai Kiran Polisetty Date: Fri, 10 Oct 2025 10:57:18 +0530 Subject: [PATCH 089/230] fix: Improve input size validation for large HTTP JSON requests (#8432) --- .../cuda_shared_memory_test.py | 8 +- qa/L0_http/http_input_size_limit_test.py | 75 ++++++++++++++++--- qa/L0_http/http_test.py | 7 +- qa/L0_http/test.sh | 8 ++ src/http_server.cc | 10 +++ 5 files changed, 94 insertions(+), 14 deletions(-) diff --git a/qa/L0_cuda_shared_memory/cuda_shared_memory_test.py b/qa/L0_cuda_shared_memory/cuda_shared_memory_test.py index cce6f4f63e..d6cf97ff1a 100755 --- a/qa/L0_cuda_shared_memory/cuda_shared_memory_test.py +++ b/qa/L0_cuda_shared_memory/cuda_shared_memory_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -723,7 +723,11 @@ def test_exceeds_cshm_handle_size_limit(self): try: error_message = response.json().get("error", "") self.assertIn( - "'raw_handle' exceeds the maximum allowed data size limit INT_MAX", + "Request JSON size", + error_message, + ) + self.assertIn( + "exceeds the maximum allowed value", error_message, ) except ValueError: diff --git a/qa/L0_http/http_input_size_limit_test.py b/qa/L0_http/http_input_size_limit_test.py index 35c9d34c0f..ca650e7fe3 100755 --- a/qa/L0_http/http_input_size_limit_test.py +++ b/qa/L0_http/http_input_size_limit_test.py @@ -29,6 +29,7 @@ sys.path.append("../common") +import json import unittest import numpy as np @@ -39,6 +40,7 @@ # Each FP32 value is 4 bytes, so we need to divide target byte sizes by 4 to get element counts BYTES_PER_FP32 = 4 MB = 2**20 # 1 MB = 1,048,576 bytes +GB = 2**30 # 1 GB = 1,073,741,824 bytes DEFAULT_LIMIT_BYTES = 64 * MB # 64MB default limit INCREASED_LIMIT_BYTES = 128 * MB # 128MB increased limit @@ -167,8 +169,11 @@ def test_default_limit_rejection_json(self): ) # Test case 2: Input just under the 64MB limit (should succeed) - # (2^24 - 32) elements * 4 bytes = 64MB - 128 bytes = 67,108,736 bytes - shape_size = DEFAULT_LIMIT_ELEMENTS - OFFSET_ELEMENTS + # The test creates a JSON payload with data, which adds overhead compared + # to raw binary format. We adjust the shape size to ensure the final + # JSON payload is under the size limit. An element is roughly 5 + # bytes in JSON, compared to 4 bytes as a raw FP32. + shape_size = (DEFAULT_LIMIT_ELEMENTS - OFFSET_ELEMENTS) * 4 // 5 payload = { "inputs": [ @@ -180,9 +185,8 @@ def test_default_limit_rejection_json(self): } ] } - assert ( - shape_size * BYTES_PER_FP32 < 64 * MB - ) # Verify we're actually under the 64MB limit + # Verify we're actually under the 64MB limit + self.assertLess(len(json.dumps(payload).encode("utf-8")), DEFAULT_LIMIT_BYTES) response = requests.post( self._get_infer_url(model), headers=headers, json=payload @@ -320,8 +324,11 @@ def test_large_input_json(self): ) # Test case 2: Input just under the 128MB configured limit (should succeed) - # (2^25 - 32) elements * 4 bytes = 128MB - 128 bytes = 134,217,600 bytes - shape_size = INCREASED_LIMIT_ELEMENTS - OFFSET_ELEMENTS + # The test creates a JSON payload with data, which adds overhead compared + # to raw binary format. We adjust the shape size to ensure the final + # JSON payload is under the size limit. An element is roughly 5 + # bytes in JSON, compared to 4 bytes as a raw FP32. + shape_size = (INCREASED_LIMIT_ELEMENTS - OFFSET_ELEMENTS) * 4 // 5 payload = { "inputs": [ @@ -333,9 +340,8 @@ def test_large_input_json(self): } ] } - assert ( - shape_size * BYTES_PER_FP32 < 128 * MB - ) # Verify we're actually under the 128MB limit + # Verify we're actually under the 128MB limit + self.assertLess(len(json.dumps(payload).encode("utf-8")), INCREASED_LIMIT_BYTES) response = requests.post( self._get_infer_url(model), headers=headers, json=payload @@ -360,6 +366,55 @@ def test_large_input_json(self): f"Expected shape {[1, shape_size]}, got {result['outputs'][0]['shape']}", ) + def test_large_string_in_json(self): + """Test JSON request with large string input""" + model = "simple_identity" + + # Create a string that is larger (large payload about 2GB) than the default limit of 64MB + # (2^31 + 64) elements * 1 bytes = 2GB + 64 bytes = 2,147,483,712 bytes + large_string_size = 2 * GB + 64 + large_string = "A" * large_string_size + + payload = { + "inputs": [ + { + "name": "INPUT0", + "datatype": "BYTES", + "shape": [1, 1], + "data": [large_string], + } + ] + } + + headers = {"Content-Type": "application/json"} + response = requests.post( + self._get_infer_url(model), headers=headers, json=payload + ) + + # Should fail with 400 bad request + self.assertEqual( + 400, + response.status_code, + "Expected error code for oversized JSON request, got: {}".format( + response.status_code + ), + ) + + # Verify error message + error_msg = response.content.decode() + self.assertIn( + "Request JSON size", + error_msg, + ) + self.assertIn( + "exceeds the maximum allowed value", + error_msg, + ) + self.assertIn( + "Use --http-max-input-size to increase the limit", + error_msg, + ) + if __name__ == "__main__": unittest.main() diff --git a/qa/L0_http/http_test.py b/qa/L0_http/http_test.py index 6786545f2f..775e1aba36 100755 --- a/qa/L0_http/http_test.py +++ b/qa/L0_http/http_test.py @@ -364,8 +364,11 @@ def test_loading_large_invalid_model(self): try: error_message = response.json().get("error", "") self.assertIn( - "'file:1/model.onnx' exceeds the maximum allowed data size limit " - "INT_MAX", + "Request JSON size", + error_message, + ) + self.assertIn( + "exceeds the maximum allowed value", error_message, ) except ValueError: diff --git a/qa/L0_http/test.sh b/qa/L0_http/test.sh index 0fc25501a3..f34e648e3c 100755 --- a/qa/L0_http/test.sh +++ b/qa/L0_http/test.sh @@ -760,6 +760,7 @@ MODELDIR=http_input_size_limit_test_models mkdir -p $MODELDIR rm -rf ${MODELDIR}/* cp -r $DATADIR/qa_identity_model_repository/onnx_zero_1_float32 ${MODELDIR}/. +cp -r ./models/simple_identity ${MODELDIR}/. # First run with default size limit - large inputs should fail SERVER_ARGS="--model-repository=${MODELDIR}" @@ -787,6 +788,13 @@ if [ $? -ne 0 ]; then echo -e "\n***\n*** Default Input Size Limit Test Failed for JSON input\n***" RET=1 fi + +python http_input_size_limit_test.py InferSizeLimitTest.test_large_string_in_json >> $CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Default Input Size Limit Test Failed for large string in JSON\n***" + RET=1 +fi set -e kill $SERVER_PID diff --git a/src/http_server.cc b/src/http_server.cc index 3c014d8e7d..860cb06350 100644 --- a/src/http_server.cc +++ b/src/http_server.cc @@ -3070,6 +3070,16 @@ HTTPAPIServer::EVBufferToJson( triton::common::TritonJson::Value* document, evbuffer_iovec* v, int* v_idx, const size_t length, int n) { + if (length > max_input_size_) { + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + ("Request JSON size of " + std::to_string(length) + + " bytes exceeds the maximum allowed value of " + + std::to_string(max_input_size_) + + " bytes. Use --http-max-input-size to increase the limit.") + .c_str()); + } + size_t offset = 0, remaining_length = length; char* json_base; std::vector json_buffer; From c40a3f52e612366eab139d0c39a57e40a42fde99 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Fri, 10 Oct 2025 08:36:13 -0700 Subject: [PATCH 090/230] Removing TensorFlow obsolete tests scenarios. (#8450) --- qa/L0_perf_deeprecommender/test.sh | 21 ++--------- qa/L0_perf_resnet/test.sh | 60 ++++++------------------------ 2 files changed, 15 insertions(+), 66 deletions(-) diff --git a/qa/L0_perf_deeprecommender/test.sh b/qa/L0_perf_deeprecommender/test.sh index dc61d56e98..bfc950aa2b 100755 --- a/qa/L0_perf_deeprecommender/test.sh +++ b/qa/L0_perf_deeprecommender/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -88,24 +88,9 @@ if [ $? -ne 0 ]; then fi rm tensorrt_models/deeprecommender_plan/model.onnx -OPTIMIZED_MODEL_NAMES="deeprecommender_graphdef_trt" - -# Create optimized models (TF-TRT and ONNX-TRT) -rm -fr optimized_model_store && mkdir optimized_model_store -for MODEL_NAME in $OPTIMIZED_MODEL_NAMES; do - BASE_MODEL=$(echo ${MODEL_NAME} | cut -d '_' -f 1,2) - cp -r $REPODIR/perf_model_store/${BASE_MODEL} optimized_model_store/${MODEL_NAME} - CONFIG_PATH="optimized_model_store/${MODEL_NAME}/config.pbtxt" - sed -i "s/^name: \"${BASE_MODEL}\"/name: \"${MODEL_NAME}\"/" ${CONFIG_PATH} - echo "optimization { execution_accelerators {" >> ${CONFIG_PATH} - echo "gpu_execution_accelerator : [ {" >> ${CONFIG_PATH} - echo "name : \"tensorrt\" " >> ${CONFIG_PATH} - echo "} ]" >> ${CONFIG_PATH} - echo "}}" >> ${CONFIG_PATH} -done # Tests with each model -for FRAMEWORK in graphdef plan graphdef_trt onnx libtorch; do +for FRAMEWORK in plan onnx libtorch; do MODEL_NAME=${MODEL}_${FRAMEWORK} if [ "$FRAMEWORK" == "plan" ]; then REPO=`pwd`/tensorrt_models @@ -173,7 +158,7 @@ fi rm tensorrt_models/deeprecommender_plan/model.onnx # Tests with each model -for FRAMEWORK in graphdef plan graphdef_trt onnx libtorch; do +for FRAMEWORK in plan onnx libtorch; do MODEL_NAME=${MODEL}_${FRAMEWORK} if [ "$FRAMEWORK" == "plan" ]; then REPO=`pwd`/tensorrt_models diff --git a/qa/L0_perf_resnet/test.sh b/qa/L0_perf_resnet/test.sh index 11538db4ce..8a559ed053 100755 --- a/qa/L0_perf_resnet/test.sh +++ b/qa/L0_perf_resnet/test.sh @@ -43,15 +43,12 @@ rm -f *.log *.csv *.tjson *.json PROTOCOLS="grpc http triton_c_api" TRT_MODEL_NAME="resnet50_fp32_plan" -TF_MODEL_NAME="resnet50v1.5_fp16_savedmodel" PYT_MODEL_NAME="resnet50_fp32_libtorch" ONNX_MODEL_NAME="resnet50_fp32_onnx" # The base model name should be the prefix to the # respective optimized model name. -TFTRT_MODEL_NAME="resnet50v1.5_fp16_savedmodel_trt" ONNXTRT_MODEL_NAME="resnet50_fp32_onnx_trt" -TFAMP_MODEL_NAME="resnet50v1.5_fp16_savedmodel_amp" ARCH=${ARCH:="x86_64"} REPODIR=${REPODIR:="/data/inferenceserver/${REPO_VERSION}"} @@ -67,15 +64,10 @@ STATIC_BATCH=1 INSTANCE_CNT=1 CONCURRENCY=1 -MODEL_NAMES="${TRT_MODEL_NAME} ${TF_MODEL_NAME} ${ONNX_MODEL_NAME} ${PYT_MODEL_NAME}" +MODEL_NAMES="${TRT_MODEL_NAME} ${ONNX_MODEL_NAME} ${PYT_MODEL_NAME}" + +OPTIMIZED_MODEL_NAMES="${ONNXTRT_MODEL_NAME}" -# Disable TF-TRT test on Jetson due to Segfault -# Disable ORT-TRT test on Jetson due to support being disabled -if [ "$ARCH" == "aarch64" ]; then - OPTIMIZED_MODEL_NAMES="${TFAMP_MODEL_NAME}" -else - OPTIMIZED_MODEL_NAMES="${TFTRT_MODEL_NAME} ${TFAMP_MODEL_NAME} ${ONNXTRT_MODEL_NAME}" -fi # Create optimized models rm -fr optimized_model_store && mkdir optimized_model_store @@ -86,21 +78,15 @@ for MODEL_NAME in $OPTIMIZED_MODEL_NAMES; do sed -i "s/^name: \"${BASE_MODEL}\"/name: \"${MODEL_NAME}\"/" ${CONFIG_PATH} echo "optimization { execution_accelerators {" >> ${CONFIG_PATH} echo "gpu_execution_accelerator : [ {" >> ${CONFIG_PATH} - if [ "${MODEL_NAME}" = "${TFAMP_MODEL_NAME}" ] ; then - echo "name : \"auto_mixed_precision\" " >> ${CONFIG_PATH} - else - echo "name : \"tensorrt\" " >> ${CONFIG_PATH} - if [ "${MODEL_NAME}" = "${TFTRT_MODEL_NAME}" ] ; then - echo "parameters { key: \"precision_mode\" value: \"FP16\" }" >> ${CONFIG_PATH} - fi - - if [ "${MODEL_NAME}" = "${ONNXTRT_MODEL_NAME}" ] ; then - echo "parameters { key: \"precision_mode\" value: \"FP16\" }" >> ${CONFIG_PATH} - echo "parameters { key: \"max_workspace_size_bytes\" value: \"1073741824\" }" >> ${CONFIG_PATH} - echo "parameters { key: \"trt_engine_cache_enable\" value: \"1\" }" >> ${CONFIG_PATH} - echo "parameters { key: \"trt_engine_cache_path\" value: \"${CACHE_PATH}\" } " >> ${CONFIG_PATH} - fi + echo "name : \"tensorrt\" " >> ${CONFIG_PATH} + + if [ "${MODEL_NAME}" = "${ONNXTRT_MODEL_NAME}" ] ; then + echo "parameters { key: \"precision_mode\" value: \"FP16\" }" >> ${CONFIG_PATH} + echo "parameters { key: \"max_workspace_size_bytes\" value: \"1073741824\" }" >> ${CONFIG_PATH} + echo "parameters { key: \"trt_engine_cache_enable\" value: \"1\" }" >> ${CONFIG_PATH} + echo "parameters { key: \"trt_engine_cache_path\" value: \"${CACHE_PATH}\" } " >> ${CONFIG_PATH} fi + echo "} ]" >> ${CONFIG_PATH} echo "}}" >> ${CONFIG_PATH} done @@ -213,26 +199,4 @@ for MODEL_NAME in $OPTIMIZED_MODEL_NAMES; do ARCH=${ARCH} \ bash -x run_test.sh done -done - -# FIXME Disable the following due to -# https://jirasw.nvidia.com/browse/DLIS-2933. -# -# Needs this additional test configuration for comparing against TFS. -if [ "$ARCH" == "x86_64" ]; then - MODEL_NAME=${TF_MODEL_NAME} - REPO=$REPODIR/perf_model_store - STATIC_BATCH=128 - INSTANCE_CNT=1 - CONCURRENCY=1 - FRAMEWORK=$(echo ${MODEL_NAME} | cut -d '_' -f 3) - MODEL_NAME=${MODEL_NAME} \ - MODEL_FRAMEWORK=${FRAMEWORK} \ - MODEL_PATH="$REPO/${MODEL_NAME}" \ - STATIC_BATCH=${STATIC_BATCH} \ - PERF_CLIENT_PROTOCOL="grpc" \ - INSTANCE_CNT=${INSTANCE_CNT} \ - CONCURRENCY=${CONCURRENCY} \ - ARCH=${ARCH} \ - bash -x run_test.sh -fi +done \ No newline at end of file From c50245cb5029f4f3352ed63a0e960d7e8e49e4f9 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Fri, 10 Oct 2025 14:21:33 -0700 Subject: [PATCH 091/230] Installing missed package (#8454) --- qa/common/gen_qa_model_repository | 1 + 1 file changed, 1 insertion(+) diff --git a/qa/common/gen_qa_model_repository b/qa/common/gen_qa_model_repository index 14d6b85011..9ac88218e5 100755 --- a/qa/common/gen_qa_model_repository +++ b/qa/common/gen_qa_model_repository @@ -326,6 +326,7 @@ cat > $TORCHSCRIPT < Date: Fri, 10 Oct 2025 15:22:20 -0700 Subject: [PATCH 092/230] Update default branch to track development of 2.63.0 / 25.11 (#8455) --- TRITON_VERSION | 2 +- build.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/TRITON_VERSION b/TRITON_VERSION index d9a000c4ed..867f66f09b 100644 --- a/TRITON_VERSION +++ b/TRITON_VERSION @@ -1 +1 @@ -2.62.0dev +2.63.0dev diff --git a/build.py b/build.py index 4044aa599d..03c84a7876 100755 --- a/build.py +++ b/build.py @@ -71,8 +71,8 @@ # DEFAULT_TRITON_VERSION_MAP = { - "release_version": "2.62.0dev", - "triton_container_version": "25.10dev", + "release_version": "2.63.0dev", + "triton_container_version": "25.11dev", "upstream_container_version": "25.09", "ort_version": "1.23.1", "ort_openvino_version": "2025.3.0", From c8a1bca480128d321eeabb0f1fabf014f8bcc024 Mon Sep 17 00:00:00 2001 From: Sai Kiran Polisetty Date: Mon, 3 Nov 2025 10:56:41 +0530 Subject: [PATCH 093/230] ci: Add support for `max_inflight_requests` parameter to prevent unbounded memory growth in ensemble models (#8458) Co-authored-by: Yingge He <157551214+yinggeh@users.noreply.github.com> --- docs/user_guide/decoupled_models.md | 12 +- docs/user_guide/ensemble_models.md | 63 +++- .../decoupled_producer/1/model.py | 54 ++++ .../decoupled_producer/config.pbtxt | 58 ++++ .../config.pbtxt | 75 +++++ .../ensemble_backpressure_test.py | 285 ++++++++++++++++++ qa/L0_simple_ensemble/test.sh | 131 +++++++- 7 files changed, 671 insertions(+), 7 deletions(-) create mode 100644 qa/L0_simple_ensemble/backpressure_test_models/decoupled_producer/1/model.py create mode 100644 qa/L0_simple_ensemble/backpressure_test_models/decoupled_producer/config.pbtxt create mode 100644 qa/L0_simple_ensemble/backpressure_test_models/ensemble_disabled_max_inflight_requests/config.pbtxt create mode 100755 qa/L0_simple_ensemble/ensemble_backpressure_test.py diff --git a/docs/user_guide/decoupled_models.md b/docs/user_guide/decoupled_models.md index fbe6f4c298..e8118843f3 100644 --- a/docs/user_guide/decoupled_models.md +++ b/docs/user_guide/decoupled_models.md @@ -1,5 +1,5 @@ -# [Triton Inference Server Release 25.09](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-25-09.html#rel-25-09) +# [Triton Inference Server Release 25.10](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-25-10.html#rel-25-10) -The Triton Inference Server container image, release 25.09, is available +The Triton Inference Server container image, release 25.10, is available on [NGC](https://ngc.nvidia.com/catalog/containers/nvidia:tritonserver) and is open source on [GitHub](https://github.com/triton-inference-server/server). Release notes can diff --git a/python/openai/README.md b/python/openai/README.md index 5dda869bab..572373390f 100644 --- a/python/openai/README.md +++ b/python/openai/README.md @@ -51,7 +51,7 @@ docker run -it --net=host --gpus all --rm \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ -e HF_TOKEN \ - nvcr.io/nvidia/tritonserver:25.09-vllm-python-py3 + nvcr.io/nvidia/tritonserver:25.10-vllm-python-py3 ``` 2. Launch the OpenAI-compatible Triton Inference Server: diff --git a/qa/L0_shared_memory/shared_memory_test.py b/qa/L0_shared_memory/shared_memory_test.py index 6f8ba94337..5cb23d9869 100755 --- a/qa/L0_shared_memory/shared_memory_test.py +++ b/qa/L0_shared_memory/shared_memory_test.py @@ -301,7 +301,13 @@ def test_too_big_shm(self): def test_large_shm_register_offset(self): # Test for out of bounds read vulnerability when registering system shared memory with large offset - for platform in ["python", "onnx", "libtorch", "plan", "openvino"]: + + platforms = ( + ["python", "onnx", "libtorch", "plan", "openvino"] + if os.environ.get("BACKENDS") is None + else os.environ.get("BACKENDS").split() + ) + for platform in platforms: model_name = f"{platform}_int32_int32_int32" # Test for large offset diff --git a/qa/L0_shared_memory/test.sh b/qa/L0_shared_memory/test.sh index 3f5758bf36..e33c9e3756 100755 --- a/qa/L0_shared_memory/test.sh +++ b/qa/L0_shared_memory/test.sh @@ -160,7 +160,7 @@ done # Test large system shared memory offset rm -rf models/* # prepare add_sub model of various backends -BACKENDS="python onnx libtorch plan openvino" +BACKENDS=${BACKENDS:-"python onnx libtorch plan openvino"} for backend in ${BACKENDS} ; do model="${backend}_int32_int32_int32" model_dir="models/${model}" diff --git a/qa/common/gen_jetson_trt_models b/qa/common/gen_jetson_trt_models index 9b652a6977..693d233f91 100755 --- a/qa/common/gen_jetson_trt_models +++ b/qa/common/gen_jetson_trt_models @@ -34,7 +34,7 @@ # Make all generated files accessible outside of container umask 0000 # Set the version of the models -TRITON_VERSION=${TRITON_VERSION:=25.09} +TRITON_VERSION=${TRITON_VERSION:=25.10} # Set the CUDA device to use CUDA_DEVICE=${RUNNER_ID:=0} # Set TensorRT image diff --git a/qa/common/gen_qa_custom_ops b/qa/common/gen_qa_custom_ops index 1d7f6db05a..a196fb3f8e 100755 --- a/qa/common/gen_qa_custom_ops +++ b/qa/common/gen_qa_custom_ops @@ -37,7 +37,7 @@ ## ############################################################################ -TRITON_VERSION=${TRITON_VERSION:=25.09} +TRITON_VERSION=${TRITON_VERSION:=25.10} NVIDIA_UPSTREAM_VERSION=${NVIDIA_UPSTREAM_VERSION:=$TRITON_VERSION} PYTORCH_IMAGE=${PYTORCH_IMAGE:=nvcr.io/nvidia/pytorch:$NVIDIA_UPSTREAM_VERSION-py3} UBUNTU_IMAGE=${UBUNTU_IMAGE:=ubuntu:24.04} diff --git a/qa/common/gen_qa_image_models.py b/qa/common/gen_qa_image_models.py index a32ba92f1b..970d08662c 100755 --- a/qa/common/gen_qa_image_models.py +++ b/qa/common/gen_qa_image_models.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -99,7 +99,8 @@ def export_vgg19(models_dir, model_name="model.onnx"): model.eval() dummy_input = torch.randn(1, 3, 224, 224) # (batch, channels, height, width) - # Export the model to ONNX format + # Use legacy TorchScript-based ONNX export + # TODO: Update to use new torch.export-based ONNX exporter (default dynamo=True) torch.onnx.export( model, dummy_input, @@ -107,6 +108,7 @@ def export_vgg19(models_dir, model_name="model.onnx"): input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}}, + dynamo=False, ) print(f"VGG19 model exported to: {model_path}") @@ -129,7 +131,8 @@ def export_resnet152(models_dir, model_name="model.onnx"): model.eval() dummy_input = torch.randn(1, 3, 224, 224) # (batch, channels, height, width) - # Export the model to ONNX format + # Use legacy TorchScript-based ONNX export + # TODO: Update to use new torch.export-based ONNX exporter (default dynamo=True) torch.onnx.export( model, dummy_input, @@ -137,6 +140,7 @@ def export_resnet152(models_dir, model_name="model.onnx"): input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}}, + dynamo=False, ) print(f"ResNet-152 model exported to: {model_path}") @@ -159,7 +163,8 @@ def export_resnet50(models_dir, model_name="model.onnx"): model.eval() dummy_input = torch.randn(1, 3, 224, 224) # (batch, channels, height, width) - # Export the model to ONNX format + # Use legacy TorchScript-based ONNX export + # TODO: Update to use new torch.export-based ONNX exporter (default dynamo=True) torch.onnx.export( model, dummy_input, @@ -167,6 +172,7 @@ def export_resnet50(models_dir, model_name="model.onnx"): input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}}, + dynamo=False, ) print(f"ResNet-50 model exported to: {model_path}") diff --git a/qa/common/gen_qa_model_repository b/qa/common/gen_qa_model_repository index 9ac88218e5..753f6a672d 100755 --- a/qa/common/gen_qa_model_repository +++ b/qa/common/gen_qa_model_repository @@ -48,7 +48,7 @@ ## ############################################################################ -TRITON_VERSION=${TRITON_VERSION:=25.09} +TRITON_VERSION=${TRITON_VERSION:=25.10} # ONNX. Use ONNX_OPSET 0 to use the default for ONNX version ONNX_VERSION=1.16.1 From 70420162bf4e311976b374d85b6d10a278f98297 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Tue, 4 Nov 2025 10:20:30 -0800 Subject: [PATCH 095/230] fix(pre-commit): update hooks versions (#8490) --- .github/workflows/{pre-commit.yaml => pre-commit.yml} | 10 +++++----- .pre-commit-config.yaml | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) rename .github/workflows/{pre-commit.yaml => pre-commit.yml} (89%) diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yml similarity index 89% rename from .github/workflows/pre-commit.yaml rename to .github/workflows/pre-commit.yml index 6c33d435c9..15d0b68685 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yml @@ -1,4 +1,4 @@ -# Copyright 2023-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -31,15 +31,15 @@ on: jobs: pre-commit: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5.0.0 with: fetch-depth: 2 - name: Get modified files id: modified-files run: echo "modified_files=$(git diff --name-only -r HEAD^1 HEAD | xargs)" >> $GITHUB_OUTPUT - - uses: actions/setup-python@v3 - - uses: pre-commit/action@v3.0.0 + - uses: actions/setup-python@v6.0.0 + - uses: pre-commit/action@v3.0.1 with: extra_args: --files ${{ steps.modified-files.outputs.modified_files }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 663a36d631..cd6320fe0d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -# Copyright 2023-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -25,7 +25,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. repos: -- repo: https://github.com/timothycrosley/isort +- repo: https://github.com/PyCQA/isort rev: 5.12.0 hooks: - id: isort @@ -36,7 +36,7 @@ repos: - id: black types_or: [python, cython] - repo: https://github.com/PyCQA/flake8 - rev: 5.0.4 + rev: 7.3.0 hooks: - id: flake8 args: [--max-line-length=88, --select=C,E,F,W,B,B950, --extend-ignore = E203,E501] @@ -57,7 +57,7 @@ repos: # More details about these pre-commit hooks here: # https://pre-commit.com/hooks.html - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v6.0.0 hooks: - id: check-case-conflict - id: check-executables-have-shebangs From 0eb1f23a86e149157bbf4efb84639ef84f710d22 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Wed, 5 Nov 2025 13:21:24 -0800 Subject: [PATCH 096/230] Update ONNX Runtime version (#8492) --- build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.py b/build.py index 0f42ab0589..a32199130b 100755 --- a/build.py +++ b/build.py @@ -74,7 +74,7 @@ "release_version": "2.63.0dev", "triton_container_version": "25.11dev", "upstream_container_version": "25.10", - "ort_version": "1.23.1", + "ort_version": "1.23.2", "ort_openvino_version": "2025.3.0", "standalone_openvino_version": "2025.3.0", "dcgm_version": "4.4.0-1", From f4ae90cd198b1eef3930058ea8850cda21816d38 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:25:05 -0800 Subject: [PATCH 097/230] feat: OpenAI-compatible API Endpoints for Embedding Models (vLLM) (#8483) --- python/openai/README.md | 94 +++ .../openai/openai_frontend/engine/engine.py | 10 +- .../openai_frontend/engine/triton_engine.py | 147 ++++- .../openai_frontend/engine/utils/triton.py | 66 +- .../fastapi/middleware/api_restriction.py | 6 +- .../frontend/fastapi/routers/embeddings.py | 50 ++ .../frontend/fastapi_frontend.py | 9 +- .../openai/openai_frontend/schemas/openai.py | 83 ++- python/openai/tests/conftest.py | 121 ++-- python/openai/tests/test_chat_completions.py | 2 - python/openai/tests/test_completions.py | 1 - python/openai/tests/test_embeddings.py | 609 ++++++++++++++++++ .../all-MiniLM-L6-v2/1/model.json | 1 + .../all-MiniLM-L6-v2/config.pbtxt | 28 + 14 files changed, 1134 insertions(+), 93 deletions(-) create mode 100644 python/openai/openai_frontend/frontend/fastapi/routers/embeddings.py create mode 100644 python/openai/tests/test_embeddings.py create mode 100644 python/openai/tests/vllm_embedding_models/all-MiniLM-L6-v2/1/model.json create mode 100644 python/openai/tests/vllm_embedding_models/all-MiniLM-L6-v2/config.pbtxt diff --git a/python/openai/README.md b/python/openai/README.md index 572373390f..bcabf5e969 100644 --- a/python/openai/README.md +++ b/python/openai/README.md @@ -301,6 +301,98 @@ See the [vLLM documentation](https://github.com/triton-inference-server/vllm_backend/blob/main/docs/llama_multi_lora_tutorial.md) on how to serve a model with LoRA adapters. +### Embedding Models +Currently, OpenAI-Compatible Frontend supports loading embedding models and embeddings endpoints via vLLM backend. Check [vLLM supported models](https://docs.vllm.ai/en/latest/models/supported_models.html#embedding) for all supported embedding models from vLLM. + +1. Launch the container and install dependencies: + - Mounts the `~/.huggingface/cache` for re-use of downloaded models across runs, containers, etc. + - Sets the [`HF_TOKEN`](https://huggingface.co/docs/huggingface_hub/en/package_reference/environment_variables#hftoken) environment variable to + access gated models, make sure this is set in your local environment if needed. + +```bash +docker run -it --net=host --gpus all --rm \ + -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ + -e HF_TOKEN \ + nvcr.io/nvidia/tritonserver:25.10-vllm-python-py3 +``` + +2. Launch the OpenAI-compatible Triton Inference Server: +```bash +cd /opt/tritonserver/python/openai + +# NOTE: Embeddings endpoint does not require "--tokenizer" +python3 openai_frontend/main.py --model-repository tests/vllm_embedding_models +``` + +

+Example output + +``` +... ++------------------+---------+--------+ +| Model | Version | Status | ++------------------+---------+--------+ +| all-MiniLM-L6-v2 | 1 | READY | <- Correct Model Loaded in Triton ++------------------+---------+--------+ +... +Found model: name='all-MiniLM-L6-v2', backend='vllm' +[WARNING] Adding CORS for the following origins: ['http://localhost'] +INFO: Started server process [133] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Uvicorn running on http://0.0.0.0:9000 (Press CTRL+C to quit) <- OpenAI Frontend Started Successfully +``` + +
+ +3. Send a `/v1/embeddings` request: + - Note the use of `jq` is optional, but provides a nicely formatted output for JSON responses. +```bash +MODEL="all-MiniLM-L6-v2" +curl -s http://localhost:9000/v1/embeddings \ + -H 'Content-Type: application/json' \ + -d '{ + "model": "'${MODEL}'", + "input": "The food was delicious and the waiter...", + "dimensions": 10, + "encoding_format": "float" + }' | jq +``` + +
+Example output + +```json +{ + "object": "list", + "data": [ + { + "object": "embedding", + "embedding": [ + -0.1914404183626175, + 0.4000193178653717, + 0.058502197265625, + 0.18909454345703125, + -0.4690297544002533, + 0.004936536308377981, + 0.45893096923828125, + -0.31141534447669983, + 0.18299102783203125, + -0.4907582700252533 + ], + "index": 0 + } + ], + "model": "all-MiniLM-L6-v2", + "usage": { + "prompt_tokens": 12, + "total_tokens": 12 + } +} +``` + +
+ ## TensorRT-LLM 0. Prepare your model repository for a TensorRT-LLM model, build the engine, etc. You can try any of the following options: @@ -655,6 +747,8 @@ Use the `--openai-restricted-api` command-line argument to configure endpoint re - **inference**: Chat completions and text completions endpoints - `POST /v1/chat/completions` - `POST /v1/completions` + - **embedding**: Embedding endpoint + - `POST /v1/embeddings` - **model-repository**: Model listing and information endpoints - `GET /v1/models` - `GET /v1/models/{model_name}` diff --git a/python/openai/openai_frontend/engine/engine.py b/python/openai/openai_frontend/engine/engine.py index 9c90dec25e..2dfeafb1db 100644 --- a/python/openai/openai_frontend/engine/engine.py +++ b/python/openai/openai_frontend/engine/engine.py @@ -1,4 +1,4 @@ -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -34,6 +34,8 @@ CreateChatCompletionResponse, CreateCompletionRequest, CreateCompletionResponse, + CreateEmbeddingRequest, + CreateEmbeddingResponse, Model, ) @@ -92,3 +94,9 @@ def completion( If request.stream is False, this returns a CreateCompletionResponse. """ pass + + def embedding(self, request: CreateEmbeddingRequest) -> CreateEmbeddingResponse: + """ + Returns a CreateEmbeddingResponse. + """ + pass diff --git a/python/openai/openai_frontend/engine/triton_engine.py b/python/openai/openai_frontend/engine/triton_engine.py index 499cc623e7..f0d184cca2 100644 --- a/python/openai/openai_frontend/engine/triton_engine.py +++ b/python/openai/openai_frontend/engine/triton_engine.py @@ -27,6 +27,7 @@ from __future__ import annotations +import base64 import json import time import uuid @@ -38,18 +39,24 @@ Callable, Dict, List, + Literal, Optional, Tuple, + Union, ) +import numpy as np import tritonserver from engine.engine import LLMEngine from engine.utils.chat import load_chat_template, parse_chat_messages from engine.utils.tokenizer import get_tokenizer from engine.utils.tool_call_parsers import ToolCallParser, ToolParserManager from engine.utils.triton import ( - _create_trtllm_inference_request, - _create_vllm_inference_request, + RequestKind, + _create_trtllm_embedding_request, + _create_trtllm_generate_request, + _create_vllm_embedding_request, + _create_vllm_generate_request, _get_output, _get_usage_from_response, _get_vllm_lora_names, @@ -73,6 +80,9 @@ CreateChatCompletionStreamResponse, CreateCompletionRequest, CreateCompletionResponse, + CreateEmbeddingRequest, + CreateEmbeddingResponse, + EmbeddingObject, FinishReason, Function1, Function2, @@ -97,7 +107,8 @@ class TritonModelMetadata: # Time that model was loaded by Triton create_time: int # Conversion format between OpenAI and Triton requests - request_converter: Callable + inference_request_converter: Callable + embedding_request_converter: Callable class TritonLLMEngine(LLMEngine): @@ -189,7 +200,7 @@ async def chat( # Convert to Triton request format and perform inference responses = metadata.model.async_infer( - metadata.request_converter( + metadata.inference_request_converter( metadata.model, prompt, request, lora_name, self.default_max_tokens ) ) @@ -232,7 +243,9 @@ async def chat( backend=metadata.backend, ) - usage = _get_usage_from_response(response, metadata.backend) + usage = _get_usage_from_response( + response, metadata.backend, RequestKind.GENERATION + ) return CreateChatCompletionResponse( id=request_id, @@ -311,7 +324,7 @@ async def completion( # Convert to Triton request format and perform inference responses = metadata.model.async_infer( - metadata.request_converter( + metadata.inference_request_converter( metadata.model, request.prompt, request, @@ -334,7 +347,9 @@ async def completion( response = responses[0] text = _get_output(response) - usage = _get_usage_from_response(response, metadata.backend) + usage = _get_usage_from_response( + response, metadata.backend, RequestKind.GENERATION + ) choice = Choice( finish_reason=FinishReason.stop, @@ -352,6 +367,57 @@ async def completion( usage=usage, ) + async def embedding( + self, request: CreateEmbeddingRequest + ) -> CreateEmbeddingResponse: + # Validate request and convert to Triton format + model_name, _ = self._get_model_and_lora_name(request.model) + metadata = self.model_metadata.get(model_name) + self._validate_embedding_request(request, metadata) + + # Convert to Triton request format and perform inference + responses = metadata.model.async_infer( + metadata.embedding_request_converter( + metadata.model, + request, + ) + ) + + # Response validation with decoupled models in mind + responses = [response async for response in responses] + _validate_triton_responses_non_streaming(responses) + response = responses[0] + + # Extract embedding from response (currently stored as JSON string in text_output) + embedding_json = _get_output(response) + embedding_list = json.loads(embedding_json) + + usage = _get_usage_from_response( + response, metadata.backend, RequestKind.EMBEDDING + ) + + embedding = self._get_embedding(embedding_list, request.encoding_format) + embedding_obj = EmbeddingObject( + embedding=embedding, index=0, object="embedding" + ) + + return CreateEmbeddingResponse( + object="list", + data=[embedding_obj], + model=request.model, + usage=usage, + ) + + @staticmethod + def _get_embedding( + embedding: List[float], encoding_format: Literal["float", "base64"] + ) -> Union[list[float], str]: + if encoding_format == "float": + return embedding + else: + embedding_bytes = np.array(embedding, dtype="float32").tobytes() + return base64.b64encode(embedding_bytes).decode("utf-8") + # TODO: This behavior should be tested further def _get_first_response_role( self, conversation: List[Dict], add_generation_prompt: bool, default_role: str @@ -362,18 +428,24 @@ def _get_first_response_role( return conversation[-1]["role"] # TODO: Expose explicit flag to catch edge cases - def _determine_request_converter(self, backend: str): + def _determine_request_converter(self, backend: str, request_type: RequestKind): # Allow manual override of backend request format if provided by user if self.backend: backend = self.backend # Request conversion from OpenAI format to backend-specific format if backend == "vllm": - return _create_vllm_inference_request + if request_type == RequestKind.GENERATION: + return _create_vllm_generate_request + else: + return _create_vllm_embedding_request # Use TRT-LLM format as default for everything else. This could be # an ensemble, a python or BLS model, a TRT-LLM backend model, etc. - return _create_trtllm_inference_request + if request_type == RequestKind.GENERATION: + return _create_trtllm_generate_request + else: + return _create_trtllm_embedding_request def _get_model_and_lora_name(self, request_model_name: str): if self.lora_separator is None or len(self.lora_separator) == 0: @@ -418,7 +490,12 @@ def _get_model_metadata(self) -> Dict[str, TritonModelMetadata]: tokenizer=self.tokenizer, lora_names=lora_names, create_time=self.create_time, - request_converter=self._determine_request_converter(backend), + inference_request_converter=self._determine_request_converter( + backend, RequestKind.GENERATION + ), + embedding_request_converter=self._determine_request_converter( + backend, RequestKind.EMBEDDING + ), ) model_metadata[name] = metadata @@ -671,8 +748,15 @@ def _validate_chat_request( if not metadata.backend: raise Exception("Unknown backend") - if not metadata.request_converter: - raise Exception(f"Unknown request format for model: {request.model}") + if not metadata.inference_request_converter: + raise Exception( + f"Unknown inference request format for model: {request.model}" + ) + + if not metadata.embedding_request_converter: + raise Exception( + f"Unknown embedding request format for model: {request.model}" + ) if ( metadata.lora_names is not None @@ -807,8 +891,15 @@ def _validate_completion_request( if not metadata.backend: raise Exception("Unknown backend") - if not metadata.request_converter: - raise Exception(f"Unknown request format for model: {request.model}") + if not metadata.inference_request_converter: + raise Exception( + f"Unknown inference request format for model: {request.model}" + ) + + if not metadata.embedding_request_converter: + raise Exception( + f"Unknown embedding request format for model: {request.model}" + ) if ( metadata.lora_names is not None @@ -853,6 +944,32 @@ def _validate_completion_request( "`stream_options.include_usage` is currently only supported for the vLLM backend" ) + def _validate_embedding_request( + self, + request: CreateEmbeddingRequest, + metadata: TritonModelMetadata, + ): + """ + Validates an embedding request to align with currently supported features. + """ + + # Reject missing internal information needed to do inference + if not metadata: + raise Exception(f"Unknown model: {request.model}") + + if not metadata.backend: + raise Exception("Unknown backend") + + if not metadata.inference_request_converter: + raise Exception( + f"Unknown inference request format for model: {request.model}" + ) + + if not metadata.embedding_request_converter: + raise Exception( + f"Unknown embedding request format for model: {request.model}" + ) + def _should_stream_with_auto_tool_parsing( self, request: CreateChatCompletionRequest ): diff --git a/python/openai/openai_frontend/engine/utils/triton.py b/python/openai/openai_frontend/engine/utils/triton.py index 636e58435d..6258c4b9df 100644 --- a/python/openai/openai_frontend/engine/utils/triton.py +++ b/python/openai/openai_frontend/engine/utils/triton.py @@ -28,6 +28,7 @@ import os import re from dataclasses import asdict, dataclass, field +from enum import Enum from pathlib import Path from typing import Iterable, List, Optional, Union @@ -40,10 +41,17 @@ CompletionUsage, CreateChatCompletionRequest, CreateCompletionRequest, + CreateEmbeddingRequest, + EmbeddingUsage, ) -def _create_vllm_inference_request( +class RequestKind(Enum): + GENERATION = 1 + EMBEDDING = 2 + + +def _create_vllm_generate_request( model, prompt, request: CreateChatCompletionRequest | CreateCompletionRequest, @@ -128,7 +136,7 @@ def _create_vllm_inference_request( return model.create_request(inputs=inputs) -def _create_trtllm_inference_request( +def _create_trtllm_generate_request( model, prompt, request: CreateChatCompletionRequest | CreateCompletionRequest, @@ -183,6 +191,35 @@ def _create_trtllm_inference_request( return model.create_request(inputs=inputs) +def _create_vllm_embedding_request( + model, + request: CreateEmbeddingRequest, +): + inputs = {} + embedding_request = {} + embedding_request["input"] = request.input + + pooling_params = {} + dims = request.dimensions + if dims is not None: + pooling_params["dimensions"] = [dims] + embedding_request["pooling_params"] = pooling_params + + inputs["embedding_request"] = [json.dumps(embedding_request)] + inputs["return_num_input_tokens"] = np.bool_([True]) + inputs["return_num_output_tokens"] = np.bool_([True]) + return model.create_request(inputs=inputs) + + +def _create_trtllm_embedding_request( + model, + request: CreateEmbeddingRequest, +): + raise Exception( + "TRT-LLM backend and Python backend do not support embedding requests" + ) + + def _construct_string_from_pointer(pointer: int, size: int) -> str: """Constructs a Python string from a C pointer and size.""" @@ -236,7 +273,7 @@ class _StreamingUsageAccumulator: def update(self, response: tritonserver.InferenceResponse): """Extracts usage from a response and updates the token counts.""" - usage = _get_usage_from_response(response, self.backend) + usage = _get_usage_from_response(response, self.backend, RequestKind.GENERATION) if usage: # The prompt_tokens is received with every chunk but should only be set once. if not self._prompt_tokens_set: @@ -262,7 +299,8 @@ def get_final_usage(self) -> Optional[CompletionUsage]: def _get_usage_from_response( response: tritonserver._api._response.InferenceResponse, backend: str, -) -> Optional[CompletionUsage]: + request_type: RequestKind, +) -> Optional[CompletionUsage | EmbeddingUsage]: """ Extracts token usage statistics from a Triton inference response. """ @@ -293,13 +331,19 @@ def _get_usage_from_response( ) completion_tokens = completion_tokens_ptr[0] - if prompt_tokens is not None and completion_tokens is not None: - total_tokens = prompt_tokens + completion_tokens - return CompletionUsage( - prompt_tokens=prompt_tokens, - completion_tokens=completion_tokens, - total_tokens=total_tokens, - ) + if prompt_tokens is not None: + if request_type == RequestKind.GENERATION and completion_tokens is not None: + total_tokens = prompt_tokens + completion_tokens + return CompletionUsage( + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + total_tokens=total_tokens, + ) + elif request_type == RequestKind.EMBEDDING: + return EmbeddingUsage( + prompt_tokens=prompt_tokens, + total_tokens=prompt_tokens, + ) return None diff --git a/python/openai/openai_frontend/frontend/fastapi/middleware/api_restriction.py b/python/openai/openai_frontend/frontend/fastapi/middleware/api_restriction.py index 9b73169ade..abe452a595 100644 --- a/python/openai/openai_frontend/frontend/fastapi/middleware/api_restriction.py +++ b/python/openai/openai_frontend/frontend/fastapi/middleware/api_restriction.py @@ -30,7 +30,11 @@ # Mapping of API to their corresponding HTTP endpoints ENDPOINT_MAPPING = { - "inference": ["POST /v1/chat/completions", "POST /v1/completions"], + "inference": [ + "POST /v1/chat/completions", + "POST /v1/completions", + "POST /v1/embeddings", + ], "model-repository": ["GET /v1/models"], "metrics": ["GET /metrics"], "health": ["GET /health/ready"], diff --git a/python/openai/openai_frontend/frontend/fastapi/routers/embeddings.py b/python/openai/openai_frontend/frontend/fastapi/routers/embeddings.py new file mode 100644 index 0000000000..84f2604d21 --- /dev/null +++ b/python/openai/openai_frontend/frontend/fastapi/routers/embeddings.py @@ -0,0 +1,50 @@ +# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from fastapi import APIRouter, HTTPException, Request +from fastapi.responses import StreamingResponse +from schemas.openai import CreateEmbeddingRequest, CreateEmbeddingResponse + +router = APIRouter() + + +@router.post( + "/v1/embeddings", response_model=CreateEmbeddingResponse, tags=["Embeddings"] +) +async def create_embedding( + request: CreateEmbeddingRequest, raw_request: Request +) -> CreateEmbeddingResponse | StreamingResponse: + """ + Creates embedding for the provided input text. + """ + if not raw_request.app.engine: + raise HTTPException(status_code=500, detail="No attached inference engine") + + try: + response = await raw_request.app.engine.embedding(request) + return response + except Exception as e: + raise HTTPException(status_code=400, detail=f"{e}") diff --git a/python/openai/openai_frontend/frontend/fastapi_frontend.py b/python/openai/openai_frontend/frontend/fastapi_frontend.py index 2234d84208..e5fb01deae 100644 --- a/python/openai/openai_frontend/frontend/fastapi_frontend.py +++ b/python/openai/openai_frontend/frontend/fastapi_frontend.py @@ -34,7 +34,13 @@ APIRestrictionMiddleware, RestrictedFeatures, ) -from frontend.fastapi.routers import chat, completions, models, observability +from frontend.fastapi.routers import ( + chat, + completions, + embeddings, + models, + observability, +) from frontend.frontend import OpenAIFrontend @@ -97,6 +103,7 @@ def _create_app(self): app.include_router(models.router) app.include_router(completions.router) app.include_router(chat.router) + app.include_router(embeddings.router) # NOTE: For debugging purposes, should generally be restricted or removed self._add_cors_middleware(app) diff --git a/python/openai/openai_frontend/schemas/openai.py b/python/openai/openai_frontend/schemas/openai.py index f13296163d..81ff6e93b3 100644 --- a/python/openai/openai_frontend/schemas/openai.py +++ b/python/openai/openai_frontend/schemas/openai.py @@ -31,7 +31,7 @@ from __future__ import annotations from enum import Enum -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Literal, Optional, Union from pydantic import AnyUrl, BaseModel, ConfigDict, Field, RootModel, confloat, conint @@ -601,10 +601,7 @@ class Model(BaseModel): owned_by: str = Field(..., description="The organization that owns the model.") -class CompletionUsage(BaseModel): - completion_tokens: int = Field( - ..., description="Number of tokens in the generated completion." - ) +class BaseUsage(BaseModel): prompt_tokens: int = Field(..., description="Number of tokens in the prompt.") total_tokens: int = Field( ..., @@ -612,6 +609,16 @@ class CompletionUsage(BaseModel): ) +class EmbeddingUsage(BaseUsage): + pass + + +class CompletionUsage(BaseUsage): + completion_tokens: int = Field( + ..., description="Number of tokens in the generated completion." + ) + + class Event(Enum): error = "error" @@ -940,3 +947,69 @@ class ObjectType: text_completion = Object1.text_completion chat_completion_chunk = Object4.chat_completion_chunk chat_completion = Object2.chat_completion + + +class EmbeddingObject(BaseModel): + model_config: ConfigDict = ConfigDict(extra="forbid") + + object: Literal["embedding"] = Field( + description="The object type, which is always 'embedding'.", + ) + embedding: Union[List[float], str] = Field( + ..., + description="The embedding vector, which is a list of floats or a base64-encoded string.", + ) + index: int = Field( + ..., + description="The index of the embedding in the list of embeddings.", + ) + + +class CreateEmbeddingRequest(BaseModel): + # Explicitly return errors for unknown fields. + model_config: ConfigDict = ConfigDict(extra="forbid") + + input: Union[str, List[int]] = Field( + ..., + description="Input text to embed, encoded as a string or array of tokens. To embed multiple inputs in a single request, pass an array of strings or array of token arrays.", + min_length=1, + examples=["The food was delicious and the waiter..."], + ) + model: Union[str, Model2] = Field( + ..., + description="ID of the model to use. See the [model endpoint compatibility](/docs/models/model-endpoint-compatibility) table for details on which models work with the Chat API.", + examples=["text-embedding-ada-002"], + ) + dimensions: Optional[int] = Field( + None, + description="The number of dimensions the resulting output embeddings should have. Only supported in text-embedding-3 and later models.", + ) + encoding_format: Optional[Literal["float", "base64"]] = Field( + "float", + description="The format to return the embeddings in.", + ) + user: Optional[str] = Field( + None, + description="A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](/docs/guides/safety-best-practices/end-user-ids).\n", + examples=["user-1234"], + ) + + +class CreateEmbeddingResponse(BaseModel): + model_config: ConfigDict = ConfigDict(extra="forbid") + + object: Literal["list"] = Field( + description="The object type, which is always 'list'.", + ) + data: List[EmbeddingObject] = Field( + ..., + description="The list of embeddings.", + ) + model: Union[str, Model2] = Field( + ..., + description="The model used to generate the embeddings.", + ) + usage: Optional[EmbeddingUsage] = Field( + ..., + description="The usage for the request.", + ) diff --git a/python/openai/tests/conftest.py b/python/openai/tests/conftest.py index 50ba0de4ed..5c269a78c1 100644 --- a/python/openai/tests/conftest.py +++ b/python/openai/tests/conftest.py @@ -67,41 +67,80 @@ def infer_test_model_repository(backend, tool_call_parser): return model_repository -# TODO: Refactor away from global variables -TEST_MODEL = os.environ.get("TEST_MODEL") -TEST_BACKEND = os.environ.get("TEST_BACKEND") -TEST_MODEL_REPOSITORY = os.environ.get("TEST_MODEL_REPOSITORY") +### FIXTURES - Refactored from global variables ### -TEST_TOKENIZER = os.environ.get( - "TEST_TOKENIZER", "meta-llama/Meta-Llama-3.1-8B-Instruct" -) -TEST_TOOL_CALL_PARSER = os.environ.get("TEST_TOOL_CALL_PARSER", "llama3") -TEST_PROMPT = "What is machine learning?" -TEST_MESSAGES = [{"role": "user", "content": TEST_PROMPT}] -if not TEST_BACKEND or not TEST_MODEL: - TEST_BACKEND, TEST_MODEL = infer_test_environment(TEST_TOOL_CALL_PARSER) +@pytest.fixture(scope="session") +def tool_call_parser(): + return os.environ.get("TEST_TOOL_CALL_PARSER", "llama3") -if not TEST_MODEL_REPOSITORY: - TEST_MODEL_REPOSITORY = infer_test_model_repository( - TEST_BACKEND, TEST_TOOL_CALL_PARSER - ) + +@pytest.fixture(scope="session") +def backend(tool_call_parser): + env_backend = os.environ.get("TEST_BACKEND") + env_model = os.environ.get("TEST_MODEL") + + if not env_backend or not env_model: + inferred_backend, _ = infer_test_environment(tool_call_parser) + return inferred_backend + return env_backend + + +@pytest.fixture(scope="session") +def model(tool_call_parser): + env_model = os.environ.get("TEST_MODEL") + + if not env_model: + _, inferred_model = infer_test_environment(tool_call_parser) + return inferred_model + return env_model + + +@pytest.fixture(scope="session") +def model_repository(backend, tool_call_parser): + env_repo = os.environ.get("TEST_MODEL_REPOSITORY") + + if env_repo: + return env_repo + return infer_test_model_repository(backend, tool_call_parser) + + +@pytest.fixture(scope="session") +def tokenizer_model(): + return os.environ.get("TEST_TOKENIZER", "meta-llama/Meta-Llama-3.1-8B-Instruct") + + +@pytest.fixture(scope="session") +def prompt(): + return "What is machine learning?" + + +@pytest.fixture(scope="session") +def messages(prompt): + return [{"role": "user", "content": prompt}] + + +@pytest.fixture(scope="session") +def input(prompt): + return prompt # NOTE: OpenAI client requires actual server running, and won't work # with the FastAPI TestClient. Run the server at module scope to run # only once for all the tests below. @pytest.fixture(scope="module") -def server(): +def server( + model_repository: str, tokenizer_model: str, backend: str, tool_call_parser: str +): args = [ "--model-repository", - TEST_MODEL_REPOSITORY, + model_repository, "--tokenizer", - TEST_TOKENIZER, + tokenizer_model, "--backend", - TEST_BACKEND, + backend, "--tool-call-parser", - TEST_TOOL_CALL_PARSER, + tool_call_parser, ] # TODO: Incorporate kserve frontend binding smoke tests to catch any # breakage with default values or slight cli arg variations @@ -117,47 +156,17 @@ def server(): # with arbitrary clients - you must use the TestClient returned to interact with # the "server" when "starting the server" via TestClient. @pytest.fixture(scope="class") -def fastapi_client_class_scope(): - server = setup_server(model_repository=TEST_MODEL_REPOSITORY) - app = setup_fastapi_app( - tokenizer=TEST_TOKENIZER, server=server, backend=TEST_BACKEND - ) +def fastapi_client_class_scope( + model_repository: str, tokenizer_model: str, backend: str +): + server = setup_server(model_repository=model_repository) + app = setup_fastapi_app(tokenizer=tokenizer_model, server=server, backend=backend) with TestClient(app) as test_client: yield test_client server.stop() -@pytest.fixture(scope="module") -def model_repository(): - return TEST_MODEL_REPOSITORY - - -@pytest.fixture(scope="module") -def model(): - return TEST_MODEL - - -@pytest.fixture(scope="module") -def backend(): - return TEST_BACKEND - - -@pytest.fixture(scope="module") -def tokenizer_model(): - return TEST_TOKENIZER - - -@pytest.fixture(scope="module") -def prompt(): - return TEST_PROMPT - - -@pytest.fixture(scope="module") -def messages(): - return TEST_MESSAGES - - # FIXME: In TRTLLM tests, the in-process Triton server for the FastAPI app # does not automatically release GPU memory, even after calling stop(). # The memory is only released when the entire pytest process exits. diff --git a/python/openai/tests/test_chat_completions.py b/python/openai/tests/test_chat_completions.py index 5402be451d..565146f7c7 100644 --- a/python/openai/tests/test_chat_completions.py +++ b/python/openai/tests/test_chat_completions.py @@ -35,7 +35,6 @@ from tests.utils import setup_fastapi_app, setup_server -@pytest.mark.fastapi class TestChatCompletions: @pytest.fixture(scope="class") def client(self, fastapi_client_class_scope): @@ -564,7 +563,6 @@ def test_usage_response( # For tests that won't use the same pytest fixture for server startup across # the whole class test suite. -@pytest.mark.fastapi class TestChatCompletionsTokenizers: # Re-use a single Triton server for different frontend configurations @pytest.fixture(scope="class") diff --git a/python/openai/tests/test_completions.py b/python/openai/tests/test_completions.py index 9ec3ffe7f7..31eb2f8ae6 100644 --- a/python/openai/tests/test_completions.py +++ b/python/openai/tests/test_completions.py @@ -29,7 +29,6 @@ import pytest -@pytest.mark.fastapi class TestCompletions: @pytest.fixture(scope="class") def client(self, fastapi_client_class_scope): diff --git a/python/openai/tests/test_embeddings.py b/python/openai/tests/test_embeddings.py new file mode 100644 index 0000000000..b2a21b78e9 --- /dev/null +++ b/python/openai/tests/test_embeddings.py @@ -0,0 +1,609 @@ +# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import base64 +import os +from pathlib import Path + +import numpy as np +import pytest + +# Results on A6000 GPU. The results vary slightly across GPU models. +EMBEDDING_OUTPUT_FLOAT = [ + -0.1914404183626175, + 0.4000193178653717, + 0.058502197265625, + 0.18909454345703125, + -0.4690297544002533, + 0.004936536308377981, + 0.45893096923828125, + -0.31141534447669983, + 0.18299102783203125, + -0.4907582700252533, + 0.6920369267463684, + -0.001537322998046875, + 0.1219015121459961, + -0.11682561784982681, + 0.02811431884765625, + -0.5207672119140625, + 0.4574941098690033, + -0.31097412109375, + 0.13371849060058594, + -0.4693959653377533, + -0.2766602337360382, + 0.029005050659179688, + -0.13730454444885254, + -0.18662643432617188, + 0.0063533782958984375, + 0.16905848681926727, + 0.1612701416015625, + 0.08376502990722656, + -0.09822845458984375, + -0.012738545425236225, + -0.16643650829792023, + -0.01901753805577755, + -0.0503285713493824, + 0.03994830325245857, + -0.3373819887638092, + -0.0188166294246912, + 0.374481201171875, + -0.4371846616268158, + 0.22470474243164062, + -0.011973063461482525, + 0.13784568011760712, + -0.1484222412109375, + 0.19347667694091797, + 0.11848513036966324, + 0.09258091449737549, + -0.2887814939022064, + -0.2301533967256546, + -0.12088584899902344, + 0.1941477507352829, + -0.05228869244456291, + -0.2508443295955658, + 0.15698719024658203, + -0.19024403393268585, + -0.4728952944278717, + 0.336700439453125, + 0.11721674352884293, + 0.24498240649700165, + -0.4826914370059967, + -0.119984470307827, + 0.1008249893784523, + -0.0983428955078125, + -0.0059178671799600124, + 0.2341969758272171, + 0.2725118100643158, + 0.2384185791015625, + -0.30748113989830017, + -0.17387008666992188, + -0.44342041015625, + -0.15537135303020477, + 0.27348455786705017, + 0.1131540909409523, + -0.23855717480182648, + 0.2574056088924408, + -0.12090619653463364, + -0.14412720501422882, + -0.2408244013786316, + 0.0021938085556030273, + -0.39945730566978455, + -0.555908203125, + 0.0760548934340477, + -0.1530914306640625, + -0.40975189208984375, + -0.2091045379638672, + 0.20317332446575165, + -0.20295588672161102, + -0.3643442690372467, + 0.05287488177418709, + -0.24874623119831085, + 0.11500009149312973, + 0.1661122590303421, + 0.26618704199790955, + -0.22980372607707977, + -0.202911376953125, + -0.2738393247127533, + 0.20629756152629852, + -0.24571101367473602, + -0.1486002653837204, + -0.12444128841161728, + 0.27539315819740295, + 0.41679826378822327, + -0.01199467945843935, + 0.1778361052274704, + -0.15123574435710907, + -0.0391184501349926, + -0.035979270935058594, + 0.11838880926370621, + -0.07832065969705582, + 0.15302227437496185, + -0.11540285497903824, + -0.008619308471679688, + 0.011735956184566021, + 0.41825103759765625, + 0.1798756867647171, + 0.0468953438103199, + -0.31410470604896545, + -0.28439536690711975, + 0.028476715087890625, + -0.18972015380859375, + -0.1492512971162796, + 0.23354721069335938, + 0.2631734311580658, + 0.3009694516658783, + -0.31204381585121155, + 0.17155838012695312, + -0.6126009821891785, + -0.16471035778522491, + 0.7154337763786316, + 0.0, + -0.3936564028263092, + -0.15255196392536163, + 0.24118296802043915, + -0.13930638134479523, + 0.6811599731445312, + 0.135009765625, + -0.18750762939453125, + 0.26521047949790955, + -0.1257190704345703, + 0.0532684326171875, + 0.25982680916786194, + -0.3410797119140625, + -0.189666748046875, + 0.016697248443961143, + 0.1474812775850296, + 0.085713230073452, + -0.0862935408949852, + 0.521209716796875, + 0.3840688169002533, + 0.04953320696949959, + -0.0478159599006176, + -0.3888498842716217, + 0.3243462145328522, + 0.03093973733484745, + -0.3594563901424408, + 0.16615693271160126, + -0.07209650427103043, + 0.049218177795410156, + 0.14628247916698456, + -0.10561561584472656, + 0.1696879118680954, + 0.1195220947265625, + 0.0140139264985919, + 0.08987680822610855, + 0.02198282815515995, + -0.06835142523050308, + -0.09100532531738281, + -0.3970082700252533, + -0.20552189648151398, + -0.0871327742934227, + -0.008806228637695312, + 0.10437265783548355, + 0.2754974365234375, + 0.2630208432674408, + -0.67779541015625, + 0.32654380798339844, + -0.4008077085018158, + 0.2785542905330658, + 0.16632080078125, + -0.0709940567612648, + -0.1678619384765625, + -0.11333879083395004, + 0.5577189326286316, + 0.3165779113769531, + -0.2243397980928421, + 0.08053144067525864, + 0.1904652863740921, + 0.22478973865509033, + 0.11852264404296875, + -0.2071024626493454, + 0.2380015105009079, + 0.4622955322265625, + 0.1029459610581398, + -0.30094656348228455, + 0.0351104736328125, + -0.09827486425638199, + 0.0018183389911428094, + 0.07406362146139145, + 0.18090057373046875, + 0.2231648713350296, + -0.1001536026597023, + 0.06609535217285156, + 0.0055376687087118626, + -0.02939859963953495, + -0.17679977416992188, + 0.2300567626953125, + -0.232757568359375, + -0.1863892823457718, + 0.14040501415729523, + -0.21081669628620148, + 0.4772237241268158, + 0.00708770751953125, + 0.25393548607826233, + -0.12926609814167023, + -0.21408335864543915, + 0.43414306640625, + -0.16021983325481415, + -0.6590754389762878, + 0.383026123046875, + 0.4894002377986908, + -0.5350291132926941, + 0.1563262939453125, + 0.4013887941837311, + -0.1429697722196579, + -0.1266673356294632, + 0.0, + -0.12781651318073273, + 0.5082905888557434, + -0.4895477294921875, + 0.05857785418629646, + -0.01038360595703125, + -0.4025942385196686, + -0.6376139521598816, + -0.27256616950035095, + -0.2183430939912796, + 0.13019943237304688, + -0.2378387451171875, + -0.12579791247844696, + 0.23233287036418915, + -0.1948690414428711, + -0.10780048370361328, + 0.4768002927303314, + 0.2761942446231842, + 0.09968694299459457, + -0.07807016372680664, + 0.18632762134075165, + -0.014780680648982525, + 0.18301646411418915, + 0.10943603515625, + 0.45223236083984375, + -0.24634425342082977, + 0.5127970576286316, + 0.15272267162799835, + 0.26901498436927795, + -0.8670451045036316, + -0.20471616089344025, + 0.3934173583984375, + -0.22558848559856415, + 0.14676158130168915, + -0.16282017529010773, + 0.0047810873948037624, + 0.49467912316322327, + -0.1040293350815773, + -0.13565094769001007, + -0.05704273656010628, + 0.2030487060546875, + 0.27226924896240234, + -0.16900062561035156, + 0.06879997253417969, + 0.44347524642944336, + 0.08619359880685806, + -0.1269734650850296, + -0.05267079547047615, + -0.3465728759765625, + 0.1846415251493454, + -0.0655873641371727, + 0.027518590912222862, + -0.06689834594726562, + -0.13316090404987335, + -0.3649355471134186, + -0.0573628731071949, + 0.030780792236328125, + 0.2462870329618454, + -0.0250523891299963, + 0.08964482694864273, + -0.34076571464538574, + -0.3342704772949219, + -0.000331878662109375, + 0.25020280480384827, + 0.34731578826904297, + 0.4081510007381439, + 0.0661773681640625, + 0.14612038433551788, + -0.37111154198646545, + -0.17901070415973663, + 0.0565798282623291, + -0.1689503937959671, + 0.311676025390625, + 0.06296539306640625, + 0.11648496240377426, + -0.16365115344524384, + -0.011795361526310444, + -0.4601001739501953, + 0.13840866088867188, + 0.1115519180893898, + -0.3645426332950592, + -0.182403564453125, + -0.20782725512981415, + -0.004481792449951172, + 0.0870104655623436, + -0.11704126745462418, + 0.34148290753364563, + 0.17841561138629913, + -0.2754109799861908, + -0.0867462158203125, + 0.09910837560892105, + -0.14540545642375946, + -0.10996246337890625, + -0.10946687310934067, + 0.023001352325081825, + 0.11987527459859848, + -5.960464477539063e-8, + 0.3316993713378906, + -0.025622526183724403, + -0.28015899658203125, + 0.34741735458374023, + 0.04091135784983635, + -0.34874120354652405, + 0.22758229076862335, + -0.042999267578125, + 0.0382130928337574, + 0.5654922127723694, + -0.9378255009651184, + 0.17114512622356415, + 0.13035202026367188, + 0.4369252622127533, + 0.0897369384765625, + 0.19928233325481415, + 0.33091607689857483, + -0.10624822229146957, + -0.2845611572265625, + 0.2822163999080658, + 0.1722426414489746, + 0.2111460417509079, + -0.1069692000746727, + -0.3496347963809967, + 0.15000660717487335, + 0.014147520065307617, + -0.36633554100990295, + 0.23989041149616241, + -0.06397350877523422, + 0.2501627504825592, + 0.04016287997364998, + -0.3789469301700592, + -0.4247843325138092, + 0.1515035629272461, + 0.36554718017578125, + 0.057392120361328125, + -0.3492482602596283, + -0.45532989501953125, + 0.4090474545955658, + -0.3914286196231842, + -0.4888407289981842, + 0.4746551513671875, + -0.6188761591911316, + -0.018857955932617188, + 0.02373504638671875, + 0.22691090404987335, + -0.07608286291360855, + 0.5331514477729797, + -0.27182260155677795, + 0.2309315949678421, + -0.1824493408203125, + 0.12648265063762665, + 0.2586142122745514, + -0.07648912817239761, + 0.2318166047334671, + -0.5225245356559753, + 0.133880615234375, + -0.010974247939884663, + 0.09001413732767105, + 0.2562611997127533, + 0.19260406494140625, + 0.4470011293888092, + -0.1636505126953125, + -0.3675130307674408, +] + + +@pytest.mark.skipif( + os.environ.get("IMAGE_KIND") == "TRTLLM", + reason="TRT-LLM backend does not support embedding requests", +) +class TestEmbeddings: + @pytest.fixture(scope="class") + def client(self, fastapi_client_class_scope): + yield fastapi_client_class_scope + + @pytest.fixture(scope="class") + def model(self): + # Override with embeddings-specific model + return "all-MiniLM-L6-v2" + + @pytest.fixture(scope="class") + def tokenizer_model(self): + return None + + @pytest.fixture(scope="class") + def model_repository(self): + # Override with embeddings-specific repository + return str(Path(__file__).parent / "vllm_embedding_models") + + @pytest.fixture(scope="class") + def input(self): + return "The food was delicious and the waiter..." + + def _check_embedding_response( + self, response, model, dims=len(EMBEDDING_OUTPUT_FLOAT), encoding_format="float" + ): + assert response.status_code == 200, response.json() + embedding = response.json()["data"][0]["embedding"] + assert embedding is not None + if encoding_format == "base64": + embedding = np.frombuffer(base64.b64decode(embedding), dtype=np.float32) + + # The results vary slightly across GPU models + result = np.allclose( + EMBEDDING_OUTPUT_FLOAT[:dims], embedding, rtol=0, atol=1e-3 + ) + assert ( + result + ), f"Embeddings do not match expected output\nExpect {EMBEDDING_OUTPUT_FLOAT[:dims]},\ngot{embedding}" + + assert response.json()["data"][0]["object"] == "embedding" + assert response.json()["data"][0]["index"] == 0 + assert response.json()["model"] == model + + usage = response.json().get("usage") + assert usage is not None + assert usage["prompt_tokens"] == 12 + assert usage["total_tokens"] == 12 + + @pytest.mark.parametrize( + "input", + [ + "The food was delicious and the waiter...", + [101, 1996, 2833, 2001, 12090, 1998, 1996, 15610, 1012, 1012, 1012, 102], + ], + ) + def test_embeddings_defaults(self, client, model: str, input: str): + response = client.post( + "/v1/embeddings", + json={"model": model, "input": input}, + ) + + self._check_embedding_response(response, model) + + # FIXME: Python model cannot unload gracefully if raise error. + # def test_chat_completions_defaults( + # self, client, model: str, messages: List[dict], backend: str + # ): + # response = client.post( + # "/v1/chat/completions", + # json={"model": model, "messages": messages}, + # ) + + # assert response.status_code == 400 + # assert "does not support" in response.json()["detail"] + + @pytest.mark.parametrize( + "param_key, param_value", + [ + ("dimensions", [10]), + ("encoding_format", "invalid"), + ("encoding_format", 0), + ], + ) + def test_embeddings_invalid_parameters( + self, client, param_key, param_value, model: str, input: str + ): + response = client.post( + "/v1/embeddings", + json={ + "model": model, + "input": input, + param_key: param_value, + }, + ) + + # Assert schema validation error + assert response.status_code == 422, response.json() + + @pytest.mark.parametrize("dimensions", [0, 10, 100, -1]) + @pytest.mark.parametrize("encoding_format", ["float", "base64"]) + def test_embeddings_parameters( + self, client, dimensions, encoding_format, model: str, input: str + ): + response = client.post( + "/v1/embeddings", + json={ + "model": model, + "input": input, + "dimensions": dimensions, + "encoding_format": encoding_format, + }, + ) + + self._check_embedding_response( + response, model, dims=dimensions, encoding_format=encoding_format + ) + + def test_embeddings_empty_request(self, client): + response = client.post("/v1/embeddings", json={}) + assert response.status_code == 422 + assert response.json()["detail"][0]["msg"] == "Field required" + + def test_embeddings_no_model(self, client, input: str): + response = client.post("/v1/embeddings", json={"input": input}) + assert response.status_code == 422 + assert response.json()["detail"][0]["msg"] == "Field required" + + @pytest.mark.parametrize( + "model, error_code", + [ + ("", 400), + (123, 422), + ("Invalid", 400), + (None, 422), + ], + ) + def test_embeddings_invalid_model(self, client, model: str, input, error_code: int): + print("Model:", model) + # Message validation requires min_length of 1 + response = client.post("/v1/embeddings", json={"model": model, "input": input}) + assert response.status_code == error_code + if error_code == 400: + assert response.json()["detail"] == f"Unknown model: {model}" + else: + assert ( + response.json()["detail"][0]["msg"] == "Input should be a valid string" + ) + + def test_embeddings_no_input(self, client, model: str): + response = client.post("/v1/embeddings", json={"model": model}) + assert response.status_code == 422 + + @pytest.mark.parametrize( + "input", + [ + "", + [], + ], + ) + def test_embeddings_empty_input(self, client, model: str, input): + # Message validation requires min_length of 1 + response = client.post("/v1/embeddings", json={"model": model, "input": input}) + assert response.status_code == 422 + assert ( + response.json()["detail"][0]["msg"] + == "Value should have at least 1 item after validation, not 0" + ) + + @pytest.mark.parametrize( + "input", + [ + 123, + 1.5, + 0, + None, + ], + ) + def test_embeddings_invalid_input(self, client, model: str, input): + # Message validation requires min_length of 1 + response = client.post("/v1/embeddings", json={"model": model, "input": input}) + assert response.status_code == 422 + assert response.json()["detail"][0]["msg"] == "Input should be a valid string" diff --git a/python/openai/tests/vllm_embedding_models/all-MiniLM-L6-v2/1/model.json b/python/openai/tests/vllm_embedding_models/all-MiniLM-L6-v2/1/model.json new file mode 100644 index 0000000000..2ad058c2e5 --- /dev/null +++ b/python/openai/tests/vllm_embedding_models/all-MiniLM-L6-v2/1/model.json @@ -0,0 +1 @@ +{"model": "sentence-transformers/all-MiniLM-L6-v2", "gpu_memory_utilization": 0.5} diff --git a/python/openai/tests/vllm_embedding_models/all-MiniLM-L6-v2/config.pbtxt b/python/openai/tests/vllm_embedding_models/all-MiniLM-L6-v2/config.pbtxt new file mode 100644 index 0000000000..39b3c48edb --- /dev/null +++ b/python/openai/tests/vllm_embedding_models/all-MiniLM-L6-v2/config.pbtxt @@ -0,0 +1,28 @@ +# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +backend: "vllm" +instance_group [{kind: KIND_MODEL}] From d074a3b05b2a48f34c41793225a397c6b8153818 Mon Sep 17 00:00:00 2001 From: J Wyman Date: Thu, 6 Nov 2025 12:20:18 -0500 Subject: [PATCH 098/230] Add Integer Overflow Sanity Check (#8489) Co-authored-by: Yingge He <157551214+yinggeh@users.noreply.github.com> --- src/http_server.cc | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/http_server.cc b/src/http_server.cc index 860cb06350..bfb8150ea0 100644 --- a/src/http_server.cc +++ b/src/http_server.cc @@ -606,8 +606,14 @@ ReadDataFromJsonHelper( const char* cstr; size_t len = 0; RETURN_IF_ERR(tensor_data.AsString(&cstr, &len)); - if (static_cast(*counter + len + sizeof(uint32_t)) > - expected_cnt) { + // Quick sanity check to ensure we don't write beyond `expected_cnt`. + int32_t actual_cnt = *counter + len + sizeof(uint32_t); + if (actual_cnt < 0) { + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INTERNAL, + "Unable to parse 'data' field: string length is negative"); + } + if (static_cast(actual_cnt) > expected_cnt) { return TRITONSERVER_ErrorNew( TRITONSERVER_ERROR_INTERNAL, "Shape does not match true shape of 'data' field"); From 4f98ebfd5b915a02d4280461553e117df52f757c Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Thu, 6 Nov 2025 14:30:15 -0800 Subject: [PATCH 099/230] build(dependencies): Validate boost version (#8494) --- Dockerfile.QA | 2 ++ build.py | 22 ++++++++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Dockerfile.QA b/Dockerfile.QA index 6cab754def..37a184e7e6 100644 --- a/Dockerfile.QA +++ b/Dockerfile.QA @@ -186,12 +186,14 @@ RUN cd tritonbuild/identity && \ make -j16 install # L0_backend_python test require triton_shm_monitor +ARG TRITON_BOOST_URL="https://archives.boost.io/release/1.80.0/source/boost_1_80_0.tar.gz" RUN cd tritonbuild/python && \ rm -rf install build && mkdir build && cd build && \ cmake -DCMAKE_INSTALL_PREFIX:PATH=/workspace/tritonbuild/python/install \ -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ -DTRITON_COMMON_REPO_TAG:STRING=${TRITON_COMMON_REPO_TAG} \ -DTRITON_CORE_REPO_TAG:STRING=${TRITON_CORE_REPO_TAG} \ + -DTRITON_BOOST_URL:STRING=${TRITON_BOOST_URL} \ -DTRITON_BACKEND_REPO_TAG:STRING=${TRITON_BACKEND_REPO_TAG} .. && \ make -j16 triton-shm-monitor install diff --git a/build.py b/build.py index a32199130b..a662afd21c 100755 --- a/build.py +++ b/build.py @@ -1002,14 +1002,16 @@ def create_dockerfile_buildbase_rhel(ddir, dockerfile_name, argmap): virtualenv \\ patchelf==0.17.2 \\ cmake==4.0.3 - +""" + df += f""" # Install boost version >= 1.78 for boost::span # Current libboost-dev apt packages are < 1.78, so install from tar.gz -RUN wget -O /tmp/boost.tar.gz \\ - https://archives.boost.io/release/1.80.0/source/boost_1_80_0.tar.gz \\ +RUN wget -O /tmp/boost.tar.gz {FLAGS.boost_url} \\ + && sha256sum /tmp/boost.tar.gz | grep {FLAGS.boost_sha256} \\ && (cd /tmp && tar xzf boost.tar.gz) \\ && mv /tmp/boost_1_80_0/boost /usr/include/boost """ + if FLAGS.enable_gpu: df += install_dcgm_libraries(argmap["DCGM_VERSION"], target_machine()) df += """ @@ -1113,14 +1115,15 @@ def create_dockerfile_buildbase(ddir, dockerfile_name, argmap): virtualenv \\ patchelf==0.17.2 \\ cmake==4.0.3 +""" + df += f""" # Install boost version >= 1.78 for boost::span # Current libboost-dev apt packages are < 1.78, so install from tar.gz -RUN wget -O /tmp/boost.tar.gz \\ - https://archives.boost.io/release/1.80.0/source/boost_1_80_0.tar.gz \\ +RUN wget -O /tmp/boost.tar.gz {FLAGS.boost_url} \\ + && sha256sum /tmp/boost.tar.gz | grep {FLAGS.boost_sha256} \\ && (cd /tmp && tar xzf boost.tar.gz) \\ && mv /tmp/boost_1_80_0/boost /usr/include/boost - """ if FLAGS.enable_gpu: @@ -2813,6 +2816,13 @@ def enable_all(): if FLAGS.build_secret is None: FLAGS.build_secret = [] + FLAGS.boost_url = os.getenv( + "TRITON_BOOST_URL", + "https://archives.boost.io/release/1.80.0/source/boost_1_80_0.tar.gz", + ) + FLAGS.boost_sha256 = ( + "4b2136f98bdd1f5857f1c3dea9ac2018effe65286cf251534b6ae20cc45e1847" + ) # if --enable-all is specified, then update FLAGS to enable all # settings, backends, repo-agents, caches, file systems, endpoints, etc. if FLAGS.enable_all: From 87f7d8160e18e34d41e77d6021ae3d198036f8f9 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Wed, 12 Nov 2025 10:00:50 -0800 Subject: [PATCH 100/230] feat: Improve Error Handling in OpenAI Frontend (#8500) --- .../openai_frontend/engine/triton_engine.py | 65 ++++++++++--------- .../openai_frontend/engine/utils/chat.py | 5 +- .../openai_frontend/engine/utils/triton.py | 21 +++--- .../fastapi/middleware/api_restriction.py | 3 +- .../frontend/fastapi/routers/chat.py | 13 +++- .../frontend/fastapi/routers/completions.py | 13 +++- .../frontend/fastapi/routers/embeddings.py | 11 +++- .../frontend/fastapi/routers/models.py | 15 +++-- .../frontend/fastapi/routers/observability.py | 11 ++-- python/openai/openai_frontend/utils/utils.py | 47 ++++++++++++++ python/openai/tests/test_chat_completions.py | 4 +- 11 files changed, 149 insertions(+), 59 deletions(-) create mode 100644 python/openai/openai_frontend/utils/utils.py diff --git a/python/openai/openai_frontend/engine/triton_engine.py b/python/openai/openai_frontend/engine/triton_engine.py index f0d184cca2..832c9ee710 100644 --- a/python/openai/openai_frontend/engine/triton_engine.py +++ b/python/openai/openai_frontend/engine/triton_engine.py @@ -89,6 +89,7 @@ Model, ObjectType, ) +from utils.utils import ClientError, ServerError # TODO: Improve type hints @@ -740,21 +741,21 @@ def _validate_chat_request( # Reject missing internal information needed to do inference if not metadata: - raise Exception(f"Unknown model: {request.model}") + raise ClientError(f"Unknown model: {request.model}") if not metadata.tokenizer: - raise Exception("Unknown tokenizer") + raise ServerError("Unknown tokenizer") if not metadata.backend: - raise Exception("Unknown backend") + raise ServerError("Unknown backend") if not metadata.inference_request_converter: - raise Exception( + raise ServerError( f"Unknown inference request format for model: {request.model}" ) if not metadata.embedding_request_converter: - raise Exception( + raise ServerError( f"Unknown embedding request format for model: {request.model}" ) @@ -763,28 +764,28 @@ def _validate_chat_request( and lora_name is not None and lora_name not in metadata.lora_names ): - raise Exception(f"Unknown LoRA: {lora_name}; for model: {request.model}") + raise ClientError(f"Unknown LoRA: {lora_name}; for model: {request.model}") # Reject unsupported features if requested if request.n and request.n > 1: - raise Exception( + raise ClientError( f"Received n={request.n}, but only single choice (n=1) is currently supported" ) if request.logit_bias is not None or request.logprobs: - raise Exception("logit bias and log probs not currently supported") + raise ClientError("logit bias and log probs not currently supported") self._verify_chat_tool_call_settings(request=request) if request.stream_options and not request.stream: - raise Exception("`stream_options` can only be used when `stream` is True") + raise ClientError("`stream_options` can only be used when `stream` is True") if ( request.stream_options and request.stream_options.include_usage and metadata.backend != "vllm" ): - raise Exception( + raise ClientError( "`stream_options.include_usage` is currently only supported for the vLLM backend" ) @@ -794,7 +795,7 @@ def _verify_chat_tool_call_settings(self, request: CreateChatCompletionRequest): and request.tool_choice.root == ChatCompletionToolChoiceOption1.required and not request.tools ): - raise Exception( + raise ClientError( '"required" tool choice requires CreateChatCompletionRequest.tools to be provided' ) @@ -803,7 +804,7 @@ def _verify_chat_tool_call_settings(self, request: CreateChatCompletionRequest): and isinstance(request.tool_choice.root, ChatCompletionNamedToolChoice) and not request.tools ): - raise Exception( + raise ClientError( "Named tool choice requires CreateChatCompletionRequest.tools to be provided" ) @@ -812,14 +813,16 @@ def _verify_chat_tool_call_settings(self, request: CreateChatCompletionRequest): and request.tool_choice.root == ChatCompletionToolChoiceOption1.auto and self.tool_call_parser is None ): - raise Exception('"auto" tool choice requires --tool-call-parser to be set') + raise ClientError( + '"auto" tool choice requires --tool-call-parser to be set' + ) if ( request.tool_choice is None and request.tools and self.tool_call_parser is None ): - raise Exception( + raise ClientError( "having tools in the request requires --tool-call-parser to be set" ) @@ -886,18 +889,18 @@ def _validate_completion_request( """ # Reject missing internal information needed to do inference if not metadata: - raise Exception(f"Unknown model: {request.model}") + raise ClientError(f"Unknown model: {request.model}") if not metadata.backend: - raise Exception("Unknown backend") + raise ServerError("Unknown backend") if not metadata.inference_request_converter: - raise Exception( + raise ServerError( f"Unknown inference request format for model: {request.model}" ) if not metadata.embedding_request_converter: - raise Exception( + raise ServerError( f"Unknown embedding request format for model: {request.model}" ) @@ -906,41 +909,41 @@ def _validate_completion_request( and lora_name is not None and lora_name not in metadata.lora_names ): - raise Exception(f"Unknown LoRA: {lora_name}; for model: {request.model}") + raise ClientError(f"Unknown LoRA: {lora_name}; for model: {request.model}") # Reject unsupported features if requested if request.suffix is not None: - raise Exception("suffix is not currently supported") + raise ClientError("suffix is not currently supported") if not request.prompt: - raise Exception("prompt must be non-empty") + raise ClientError("prompt must be non-empty") # Currently only support single string as input if not isinstance(request.prompt, str): - raise Exception("only single string input is supported") + raise ClientError("only single string input is supported") if request.n and request.n > 1: - raise Exception( + raise ClientError( f"Received n={request.n}, but only single choice (n=1) is currently supported" ) if request.best_of and request.best_of > 1: - raise Exception( + raise ClientError( f"Received best_of={request.best_of}, but only single choice (best_of=1) is currently supported" ) if request.logit_bias is not None or request.logprobs is not None: - raise Exception("logit bias and log probs not supported") + raise ClientError("logit bias and log probs not supported") if request.stream_options and not request.stream: - raise Exception("`stream_options` can only be used when `stream` is True") + raise ClientError("`stream_options` can only be used when `stream` is True") if ( request.stream_options and request.stream_options.include_usage and metadata.backend != "vllm" ): - raise Exception( + raise ClientError( "`stream_options.include_usage` is currently only supported for the vLLM backend" ) @@ -955,18 +958,18 @@ def _validate_embedding_request( # Reject missing internal information needed to do inference if not metadata: - raise Exception(f"Unknown model: {request.model}") + raise ClientError(f"Unknown model: {request.model}") if not metadata.backend: - raise Exception("Unknown backend") + raise ServerError("Unknown backend") if not metadata.inference_request_converter: - raise Exception( + raise ServerError( f"Unknown inference request format for model: {request.model}" ) if not metadata.embedding_request_converter: - raise Exception( + raise ServerError( f"Unknown embedding request format for model: {request.model}" ) diff --git a/python/openai/openai_frontend/engine/utils/chat.py b/python/openai/openai_frontend/engine/utils/chat.py index 3c40beecc3..2c941e666c 100644 --- a/python/openai/openai_frontend/engine/utils/chat.py +++ b/python/openai/openai_frontend/engine/utils/chat.py @@ -38,6 +38,7 @@ ChatCompletionRequestUserMessage, Type1, ) +from utils.utils import ClientError class ConversationMessage(TypedDict, total=False): @@ -80,7 +81,9 @@ def _parse_chat_message_content_parts( parse_res = {"type": "text", "text": part.root.text} content.append(parse_res) else: - raise TypeError(f"only text message is supported, but got {part.root.type}") + raise ClientError( + f"only text message is supported, but got {part.root.type}" + ) return ConversationMessage(role=role, content=content) diff --git a/python/openai/openai_frontend/engine/utils/triton.py b/python/openai/openai_frontend/engine/utils/triton.py index 6258c4b9df..a660f589e4 100644 --- a/python/openai/openai_frontend/engine/utils/triton.py +++ b/python/openai/openai_frontend/engine/utils/triton.py @@ -44,6 +44,7 @@ CreateEmbeddingRequest, EmbeddingUsage, ) +from utils.utils import ClientError, ServerError class RequestKind(Enum): @@ -144,7 +145,9 @@ def _create_trtllm_generate_request( default_max_tokens: int, ): if lora_name is not None: - raise Exception("LoRA selection is currently not supported for TRT-LLM backend") + raise ClientError( + "LoRA selection is currently not supported for TRT-LLM backend" + ) inputs = {} inputs["text_input"] = [[prompt]] @@ -215,7 +218,7 @@ def _create_trtllm_embedding_request( model, request: CreateEmbeddingRequest, ): - raise Exception( + raise ClientError( "TRT-LLM backend and Python backend do not support embedding requests" ) @@ -250,11 +253,11 @@ def _to_string(tensor: tritonserver.Tensor) -> str: # there is only a single string, so enforce it to avoid obscure errors. volume = _get_volume(tensor.shape) if volume != 1: - raise Exception( + raise ServerError( f"Expected to find 1 string in the output, found {volume} instead." ) if tensor.size < 4: - raise Exception( + raise ServerError( f"Expected string buffer to contain its serialized byte size, but found size of {tensor.size}." ) @@ -368,11 +371,13 @@ def _validate_triton_responses_non_streaming( ): num_responses = len(responses) if num_responses == 1 and responses[0].final != True: - raise Exception("Unexpected internal error with incorrect response flags") + raise ServerError("Unexpected internal error with incorrect response flags") if num_responses == 2 and responses[-1].final != True: - raise Exception("Unexpected internal error with incorrect response flags") + raise ServerError("Unexpected internal error with incorrect response flags") if num_responses > 2: - raise Exception(f"Unexpected number of responses: {num_responses}, expected 1.") + raise ServerError( + f"Unexpected number of responses: {num_responses}, expected 1." + ) def _get_guided_json_from_tool( @@ -391,7 +396,7 @@ def _get_guided_json_from_tool( tools = {tool.function.name: tool.function for tool in request.tools} if tool_name not in tools: - raise ValueError(f"Tool '{tool_name}' has not been passed in `tools`.") + raise ClientError(f"Tool '{tool_name}' has not been passed in `tools`.") tool = tools[tool_name] return tool.parameters.model_dump_json() diff --git a/python/openai/openai_frontend/frontend/fastapi/middleware/api_restriction.py b/python/openai/openai_frontend/frontend/fastapi/middleware/api_restriction.py index abe452a595..c2216f9e1d 100644 --- a/python/openai/openai_frontend/frontend/fastapi/middleware/api_restriction.py +++ b/python/openai/openai_frontend/frontend/fastapi/middleware/api_restriction.py @@ -27,6 +27,7 @@ from fastapi import Request from fastapi.responses import JSONResponse from starlette.middleware.base import BaseHTTPMiddleware +from utils.utils import StatusCode # Mapping of API to their corresponding HTTP endpoints ENDPOINT_MAPPING = { @@ -189,7 +190,7 @@ async def dispatch(self, request: Request, call_next): else: # Authentication failed, return 401 error return JSONResponse( - status_code=401, + status_code=StatusCode.AUTHORIZATION_ERROR, content={ "error": { "message": auth_result["message"], diff --git a/python/openai/openai_frontend/frontend/fastapi/routers/chat.py b/python/openai/openai_frontend/frontend/fastapi/routers/chat.py index 0f72047a5e..b7dee0c20c 100644 --- a/python/openai/openai_frontend/frontend/fastapi/routers/chat.py +++ b/python/openai/openai_frontend/frontend/fastapi/routers/chat.py @@ -1,4 +1,4 @@ -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -27,6 +27,7 @@ from fastapi import APIRouter, HTTPException, Request from fastapi.responses import StreamingResponse from schemas.openai import CreateChatCompletionRequest, CreateChatCompletionResponse +from utils.utils import ClientError, ServerError, StatusCode router = APIRouter() @@ -42,12 +43,18 @@ async def create_chat_completion( Creates a chat completion for the provided messages and parameters. """ if not raw_request.app.engine: - raise HTTPException(status_code=500, detail="No attached inference engine") + raise HTTPException( + status_code=StatusCode.SERVER_ERROR, detail="No attached inference engine" + ) try: response = await raw_request.app.engine.chat(request) if request.stream: return StreamingResponse(response, media_type="text/event-stream") return response + except ClientError as e: + raise HTTPException(status_code=StatusCode.CLIENT_ERROR, detail=f"{e}") + except ServerError as e: + raise HTTPException(status_code=StatusCode.SERVER_ERROR, detail=f"{e}") except Exception as e: - raise HTTPException(status_code=400, detail=f"{e}") + raise HTTPException(status_code=StatusCode.SERVER_ERROR, detail=f"{e}") diff --git a/python/openai/openai_frontend/frontend/fastapi/routers/completions.py b/python/openai/openai_frontend/frontend/fastapi/routers/completions.py index ade89a47cc..2aa962923e 100644 --- a/python/openai/openai_frontend/frontend/fastapi/routers/completions.py +++ b/python/openai/openai_frontend/frontend/fastapi/routers/completions.py @@ -1,4 +1,4 @@ -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -27,6 +27,7 @@ from fastapi import APIRouter, HTTPException, Request from fastapi.responses import StreamingResponse from schemas.openai import CreateCompletionRequest, CreateCompletionResponse +from utils.utils import ClientError, ServerError, StatusCode router = APIRouter() @@ -41,12 +42,18 @@ async def create_completion( Creates a completion for the provided prompt and parameters. """ if not raw_request.app.engine: - raise HTTPException(status_code=500, detail="No attached inference engine") + raise HTTPException( + status_code=StatusCode.SERVER_ERROR, detail="No attached inference engine" + ) try: response = await raw_request.app.engine.completion(request) if request.stream: return StreamingResponse(response, media_type="text/event-stream") return response + except ClientError as e: + raise HTTPException(status_code=StatusCode.CLIENT_ERROR, detail=f"{e}") + except ServerError as e: + raise HTTPException(status_code=StatusCode.SERVER_ERROR, detail=f"{e}") except Exception as e: - raise HTTPException(status_code=400, detail=f"{e}") + raise HTTPException(status_code=StatusCode.SERVER_ERROR, detail=f"{e}") diff --git a/python/openai/openai_frontend/frontend/fastapi/routers/embeddings.py b/python/openai/openai_frontend/frontend/fastapi/routers/embeddings.py index 84f2604d21..8f0bfe6771 100644 --- a/python/openai/openai_frontend/frontend/fastapi/routers/embeddings.py +++ b/python/openai/openai_frontend/frontend/fastapi/routers/embeddings.py @@ -27,6 +27,7 @@ from fastapi import APIRouter, HTTPException, Request from fastapi.responses import StreamingResponse from schemas.openai import CreateEmbeddingRequest, CreateEmbeddingResponse +from utils.utils import ClientError, ServerError, StatusCode router = APIRouter() @@ -41,10 +42,16 @@ async def create_embedding( Creates embedding for the provided input text. """ if not raw_request.app.engine: - raise HTTPException(status_code=500, detail="No attached inference engine") + raise HTTPException( + status_code=StatusCode.SERVER_ERROR, detail="No attached inference engine" + ) try: response = await raw_request.app.engine.embedding(request) return response + except ClientError as e: + raise HTTPException(status_code=StatusCode.CLIENT_ERROR, detail=f"{e}") + except ServerError as e: + raise HTTPException(status_code=StatusCode.SERVER_ERROR, detail=f"{e}") except Exception as e: - raise HTTPException(status_code=400, detail=f"{e}") + raise HTTPException(status_code=StatusCode.SERVER_ERROR, detail=f"{e}") diff --git a/python/openai/openai_frontend/frontend/fastapi/routers/models.py b/python/openai/openai_frontend/frontend/fastapi/routers/models.py index ac2fa7fdc0..39e2f26d53 100644 --- a/python/openai/openai_frontend/frontend/fastapi/routers/models.py +++ b/python/openai/openai_frontend/frontend/fastapi/routers/models.py @@ -1,4 +1,4 @@ -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -28,6 +28,7 @@ from fastapi import APIRouter, HTTPException, Request from schemas.openai import ListModelsResponse, Model, ObjectType +from utils.utils import StatusCode router = APIRouter() @@ -40,7 +41,9 @@ def list_models(request: Request) -> ListModelsResponse: Lists the currently available models, and provides basic information about each one such as the owner and availability. """ if not request.app.engine: - raise HTTPException(status_code=500, detail="No attached inference engine") + raise HTTPException( + status_code=StatusCode.SERVER_ERROR, detail="No attached inference engine" + ) models: List[Model] = request.app.engine.models() return ListModelsResponse(object=ObjectType.list, data=models) @@ -52,7 +55,9 @@ def retrieve_model(request: Request, model_name: str) -> Model: Retrieves a model instance, providing basic information about the model such as the owner and permissioning. """ if not request.app.engine: - raise HTTPException(status_code=500, detail="No attached inference engine") + raise HTTPException( + status_code=StatusCode.SERVER_ERROR, detail="No attached inference engine" + ) # TODO: Return model directly from engine instead of searching models models: List[Model] = request.app.engine.models() @@ -60,4 +65,6 @@ def retrieve_model(request: Request, model_name: str) -> Model: if model.id == model_name: return model - raise HTTPException(status_code=404, detail=f"Unknown model: {model_name}") + raise HTTPException( + status_code=StatusCode.NOT_FOUND, detail=f"Unknown model: {model_name}" + ) diff --git a/python/openai/openai_frontend/frontend/fastapi/routers/observability.py b/python/openai/openai_frontend/frontend/fastapi/routers/observability.py index b8040f56b7..ca881c632e 100644 --- a/python/openai/openai_frontend/frontend/fastapi/routers/observability.py +++ b/python/openai/openai_frontend/frontend/fastapi/routers/observability.py @@ -1,4 +1,4 @@ -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -26,6 +26,7 @@ from fastapi import APIRouter, HTTPException, Request from fastapi.responses import PlainTextResponse, Response +from utils.utils import StatusCode router = APIRouter() @@ -38,12 +39,14 @@ def metrics(request: Request) -> PlainTextResponse: @router.get("/health/ready", tags=["Utilities"]) def ready(request: Request) -> Response: if not request.app.engine: - raise HTTPException(status_code=500, detail="No attached inference engine") + raise HTTPException( + status_code=StatusCode.SERVER_ERROR, detail="No attached inference engine" + ) if not request.app.engine.ready(): raise HTTPException( - status_code=400, + status_code=StatusCode.CLIENT_ERROR, detail="Attached inference engine is not ready for inference requests.", ) - return Response(status_code=200) + return Response(status_code=StatusCode.SUCCESS) diff --git a/python/openai/openai_frontend/utils/utils.py b/python/openai/openai_frontend/utils/utils.py new file mode 100644 index 0000000000..c8fd3609f6 --- /dev/null +++ b/python/openai/openai_frontend/utils/utils.py @@ -0,0 +1,47 @@ +# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from enum import IntEnum + + +class ServerError(Exception): + """Exception raised for server errors.""" + + pass + + +class ClientError(Exception): + """Exception raised for client errors.""" + + pass + + +class StatusCode(IntEnum): + SUCCESS = 200 + CLIENT_ERROR = 400 + AUTHORIZATION_ERROR = 401 + NOT_FOUND = 404 + SERVER_ERROR = 500 diff --git a/python/openai/tests/test_chat_completions.py b/python/openai/tests/test_chat_completions.py index 565146f7c7..446a8db925 100644 --- a/python/openai/tests/test_chat_completions.py +++ b/python/openai/tests/test_chat_completions.py @@ -588,7 +588,7 @@ def test_chat_completions_no_tokenizer( json={"model": model, "messages": messages}, ) - assert response.status_code == 400 + assert response.status_code == 500 assert response.json()["detail"] == "Unknown tokenizer" def test_chat_completions_custom_tokenizer( @@ -675,7 +675,7 @@ def test_chat_completions_invalid_chat_tokenizer( json={"model": model, "messages": messages}, ) - assert response.status_code == 400 + assert response.status_code == 500 # Error may vary based on transformers version expected_errors = [ "cannot use apply_chat_template()", From 75993ddf9677b92efad8e260a82d7dc059ebd983 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Wed, 12 Nov 2025 12:44:36 -0800 Subject: [PATCH 101/230] fix: Skip model resnet50_fp32_onnx in L0_memory_growth--base (#8503) --- qa/L0_memory_growth/test.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qa/L0_memory_growth/test.sh b/qa/L0_memory_growth/test.sh index c99e931175..f23813937d 100755 --- a/qa/L0_memory_growth/test.sh +++ b/qa/L0_memory_growth/test.sh @@ -132,7 +132,9 @@ RET=0 for MODEL in $(ls models); do # Skip the resnet50_fp32_libtorch model as it is running into `misaligned address' # Tracked here: https://nvbugs/3954104 - if [ "$MODEL" == "resnet50_fp32_libtorch" ]; then + # Skip the resnet50_fp32_onnx model as the inference hangs on A100 with batch size > 1. + # Tracked here: https://linear.app/nvidia/issue/TRI-304 + if [[ "$MODEL" == "resnet50_fp32_libtorch" || "$MODEL" == "resnet50_fp32_onnx" ]]; then continue fi From a46823087e0da4307243b0854606dd39861544b1 Mon Sep 17 00:00:00 2001 From: Sai Kiran Polisetty Date: Thu, 13 Nov 2025 15:13:37 +0530 Subject: [PATCH 102/230] feat: Add support for `usage` in the OpenAI frontend TRT-LLM backend (#8326) --- python/openai/README.md | 14 +- .../openai_frontend/engine/triton_engine.py | 18 --- .../openai_frontend/engine/utils/triton.py | 19 ++- python/openai/tests/test_chat_completions.py | 18 +-- python/openai/tests/test_completions.py | 14 +- python/openai/tests/test_openai_client.py | 123 +++++------------- qa/L0_openai/test.sh | 27 +++- 7 files changed, 85 insertions(+), 148 deletions(-) diff --git a/python/openai/README.md b/python/openai/README.md index bcabf5e969..a609946493 100644 --- a/python/openai/README.md +++ b/python/openai/README.md @@ -446,13 +446,13 @@ curl -s http://localhost:9000/v1/chat/completions -H 'Content-Type: application/ ```json { - "id": "cmpl-704c758c-8a84-11ef-b106-107c6149ca79", + "id": "cmpl-5ad4f860-bf13-11f0-b137-b75b7f0a8586", "choices": [ { "finish_reason": "stop", "index": 0, "message": { - "content": "It looks like you're testing the system!", + "content": "It looks like you're ready to see if I'm functioning properly. What would", "tool_calls": null, "role": "assistant", "function_call": null @@ -460,11 +460,15 @@ curl -s http://localhost:9000/v1/chat/completions -H 'Content-Type: application/ "logprobs": null } ], - "created": 1728948689, - "model": "llama-3-8b-instruct", + "created": 1762875029, + "model": "tensorrt_llm_bls", "system_fingerprint": null, "object": "chat.completion", - "usage": null + "usage": { + "prompt_tokens": 42, + "total_tokens": 58, + "completion_tokens": 16 + } } ``` diff --git a/python/openai/openai_frontend/engine/triton_engine.py b/python/openai/openai_frontend/engine/triton_engine.py index 832c9ee710..6a96c5652d 100644 --- a/python/openai/openai_frontend/engine/triton_engine.py +++ b/python/openai/openai_frontend/engine/triton_engine.py @@ -780,15 +780,6 @@ def _validate_chat_request( if request.stream_options and not request.stream: raise ClientError("`stream_options` can only be used when `stream` is True") - if ( - request.stream_options - and request.stream_options.include_usage - and metadata.backend != "vllm" - ): - raise ClientError( - "`stream_options.include_usage` is currently only supported for the vLLM backend" - ) - def _verify_chat_tool_call_settings(self, request: CreateChatCompletionRequest): if ( request.tool_choice @@ -938,15 +929,6 @@ def _validate_completion_request( if request.stream_options and not request.stream: raise ClientError("`stream_options` can only be used when `stream` is True") - if ( - request.stream_options - and request.stream_options.include_usage - and metadata.backend != "vllm" - ): - raise ClientError( - "`stream_options.include_usage` is currently only supported for the vLLM backend" - ) - def _validate_embedding_request( self, request: CreateEmbeddingRequest, diff --git a/python/openai/openai_frontend/engine/utils/triton.py b/python/openai/openai_frontend/engine/utils/triton.py index a660f589e4..2880875a9e 100644 --- a/python/openai/openai_frontend/engine/utils/triton.py +++ b/python/openai/openai_frontend/engine/utils/triton.py @@ -189,6 +189,10 @@ def _create_trtllm_generate_request( if guided_json is not None: inputs["guided_decoding_guide_type"] = [["json_schema"]] inputs["guided_decoding_guide"] = [[guided_json]] + + inputs["return_num_input_tokens"] = np.bool_([[True]]) + inputs["return_num_output_tokens"] = np.bool_([[True]]) + # FIXME: TRT-LLM doesn't currently support runtime changes of 'echo' and it # is configured at model load time, so we don't handle it here for now. return model.create_request(inputs=inputs) @@ -307,11 +311,6 @@ def _get_usage_from_response( """ Extracts token usage statistics from a Triton inference response. """ - # TODO: Remove this check once TRT-LLM backend supports both "num_input_tokens" - # and "num_output_tokens", and also update the test cases accordingly. - if backend != "vllm": - return None - prompt_tokens = None completion_tokens = None @@ -327,12 +326,22 @@ def _get_usage_from_response( input_token_tensor.data_ptr, ctypes.POINTER(ctypes.c_uint32) ) prompt_tokens = prompt_tokens_ptr[0] + elif input_token_tensor.data_type == tritonserver.DataType.INT32: + prompt_tokens_ptr = ctypes.cast( + input_token_tensor.data_ptr, ctypes.POINTER(ctypes.c_int32) + ) + prompt_tokens = prompt_tokens_ptr[0] if output_token_tensor.data_type == tritonserver.DataType.UINT32: completion_tokens_ptr = ctypes.cast( output_token_tensor.data_ptr, ctypes.POINTER(ctypes.c_uint32) ) completion_tokens = completion_tokens_ptr[0] + elif output_token_tensor.data_type == tritonserver.DataType.INT32: + completion_tokens_ptr = ctypes.cast( + output_token_tensor.data_ptr, ctypes.POINTER(ctypes.c_int32) + ) + completion_tokens = completion_tokens_ptr[0] if prompt_tokens is not None: if request_type == RequestKind.GENERATION and completion_tokens is not None: diff --git a/python/openai/tests/test_chat_completions.py b/python/openai/tests/test_chat_completions.py index 446a8db925..2b410c3cc2 100644 --- a/python/openai/tests/test_chat_completions.py +++ b/python/openai/tests/test_chat_completions.py @@ -40,9 +40,7 @@ class TestChatCompletions: def client(self, fastapi_client_class_scope): yield fastapi_client_class_scope - def test_chat_completions_defaults( - self, client, model: str, messages: List[dict], backend: str - ): + def test_chat_completions_defaults(self, client, model: str, messages: List[dict]): response = client.post( "/v1/chat/completions", json={"model": model, "messages": messages}, @@ -54,10 +52,7 @@ def test_chat_completions_defaults( assert message["role"] == "assistant" usage = response.json().get("usage") - if backend == "vllm": - assert usage is not None - else: - assert usage is None + assert usage is not None def test_chat_completions_system_prompt(self, client, model: str): # NOTE: Currently just sanity check that there are no issues when a @@ -535,14 +530,7 @@ def test_request_logprobs(self): def test_request_logit_bias(self): pass - def test_usage_response( - self, client, model: str, messages: List[dict], backend: str - ): - if backend != "vllm": - pytest.skip( - "Usage reporting is currently available only for the vLLM backend." - ) - + def test_usage_response(self, client, model: str, messages: List[dict]): response = client.post( "/v1/chat/completions", json={"model": model, "messages": messages}, diff --git a/python/openai/tests/test_completions.py b/python/openai/tests/test_completions.py index 31eb2f8ae6..fca7521ad8 100644 --- a/python/openai/tests/test_completions.py +++ b/python/openai/tests/test_completions.py @@ -34,7 +34,7 @@ class TestCompletions: def client(self, fastapi_client_class_scope): yield fastapi_client_class_scope - def test_completions_defaults(self, client, model: str, prompt: str, backend: str): + def test_completions_defaults(self, client, model: str, prompt: str): response = client.post( "/v1/completions", json={"model": model, "prompt": prompt}, @@ -47,10 +47,7 @@ def test_completions_defaults(self, client, model: str, prompt: str, backend: st assert response.json()["choices"][0]["text"].strip() usage = response.json().get("usage") - if backend == "vllm": - assert usage is not None - else: - assert usage is None + assert usage is not None @pytest.mark.parametrize( "sampling_parameter, value", @@ -370,12 +367,7 @@ def test_lora(self): def test_multi_lora(self): pass - def test_usage_response(self, client, model: str, prompt: str, backend: str): - if backend != "vllm": - pytest.skip( - "Usage reporting is currently available only for the vLLM backend." - ) - + def test_usage_response(self, client, model: str, prompt: str): response = client.post( "/v1/completions", json={"model": model, "prompt": prompt}, diff --git a/python/openai/tests/test_openai_client.py b/python/openai/tests/test_openai_client.py index 1a1001329b..5ffcbe4f1d 100644 --- a/python/openai/tests/test_openai_client.py +++ b/python/openai/tests/test_openai_client.py @@ -49,7 +49,7 @@ def test_openai_client_models(self, client: openai.OpenAI, backend: str): raise Exception(f"Unexpected backend {backend=}") def test_openai_client_completion( - self, client: openai.OpenAI, model: str, prompt: str, backend: str + self, client: openai.OpenAI, model: str, prompt: str ): completion = client.completions.create( prompt=prompt, @@ -61,19 +61,16 @@ def test_openai_client_completion( assert completion.choices[0].finish_reason == "stop" usage = completion.usage - if backend == "vllm": - assert usage is not None - assert isinstance(usage.prompt_tokens, int) - assert isinstance(usage.completion_tokens, int) - assert isinstance(usage.total_tokens, int) - assert usage.prompt_tokens > 0 - assert usage.completion_tokens > 0 - assert usage.total_tokens == usage.prompt_tokens + usage.completion_tokens - else: - assert usage is None + assert usage is not None + assert isinstance(usage.prompt_tokens, int) + assert isinstance(usage.completion_tokens, int) + assert isinstance(usage.total_tokens, int) + assert usage.prompt_tokens > 0 + assert usage.completion_tokens > 0 + assert usage.total_tokens == usage.prompt_tokens + usage.completion_tokens def test_openai_client_chat_completion( - self, client: openai.OpenAI, model: str, messages: List[dict], backend: str + self, client: openai.OpenAI, model: str, messages: List[dict] ): chat_completion = client.chat.completions.create( messages=messages, @@ -85,16 +82,13 @@ def test_openai_client_chat_completion( assert chat_completion.choices[0].finish_reason == "stop" usage = chat_completion.usage - if backend == "vllm": - assert usage is not None - assert isinstance(usage.prompt_tokens, int) - assert isinstance(usage.completion_tokens, int) - assert isinstance(usage.total_tokens, int) - assert usage.prompt_tokens > 0 - assert usage.completion_tokens > 0 - assert usage.total_tokens == usage.prompt_tokens + usage.completion_tokens - else: - assert usage is None + assert usage is not None + assert isinstance(usage.prompt_tokens, int) + assert isinstance(usage.completion_tokens, int) + assert isinstance(usage.total_tokens, int) + assert usage.prompt_tokens > 0 + assert usage.completion_tokens > 0 + assert usage.total_tokens == usage.prompt_tokens + usage.completion_tokens @pytest.mark.parametrize("echo", [False, True]) def test_openai_client_completion_echo( @@ -141,7 +135,7 @@ async def test_openai_client_models(self, client: openai.AsyncOpenAI, backend: s @pytest.mark.asyncio async def test_openai_client_completion( - self, client: openai.AsyncOpenAI, model: str, prompt: str, backend: str + self, client: openai.AsyncOpenAI, model: str, prompt: str ): completion = await client.completions.create( prompt=prompt, @@ -153,20 +147,17 @@ async def test_openai_client_completion( assert completion.choices[0].finish_reason == "stop" usage = completion.usage - if backend == "vllm": - assert usage is not None - assert isinstance(usage.prompt_tokens, int) - assert isinstance(usage.completion_tokens, int) - assert isinstance(usage.total_tokens, int) - assert usage.prompt_tokens > 0 - assert usage.completion_tokens > 0 - assert usage.total_tokens == usage.prompt_tokens + usage.completion_tokens - else: - assert usage is None + assert usage is not None + assert isinstance(usage.prompt_tokens, int) + assert isinstance(usage.completion_tokens, int) + assert isinstance(usage.total_tokens, int) + assert usage.prompt_tokens > 0 + assert usage.completion_tokens > 0 + assert usage.total_tokens == usage.prompt_tokens + usage.completion_tokens @pytest.mark.asyncio async def test_openai_client_chat_completion( - self, client: openai.AsyncOpenAI, model: str, messages: List[dict], backend: str + self, client: openai.AsyncOpenAI, model: str, messages: List[dict] ): chat_completion = await client.chat.completions.create( messages=messages, @@ -177,16 +168,13 @@ async def test_openai_client_chat_completion( assert chat_completion.choices[0].finish_reason == "stop" usage = chat_completion.usage - if backend == "vllm": - assert usage is not None - assert isinstance(usage.prompt_tokens, int) - assert isinstance(usage.completion_tokens, int) - assert isinstance(usage.total_tokens, int) - assert usage.prompt_tokens > 0 - assert usage.completion_tokens > 0 - assert usage.total_tokens == usage.prompt_tokens + usage.completion_tokens - else: - assert usage is None + assert usage is not None + assert isinstance(usage.prompt_tokens, int) + assert isinstance(usage.completion_tokens, int) + assert isinstance(usage.total_tokens, int) + assert usage.prompt_tokens > 0 + assert usage.completion_tokens > 0 + assert usage.total_tokens == usage.prompt_tokens + usage.completion_tokens print(f"Chat completion results: {chat_completion}") @@ -300,13 +288,8 @@ async def test_chat_streaming( @pytest.mark.asyncio async def test_chat_streaming_usage_option( - self, client: openai.AsyncOpenAI, model: str, messages: List[dict], backend: str + self, client: openai.AsyncOpenAI, model: str, messages: List[dict] ): - if backend != "vllm": - pytest.skip( - "Usage reporting is currently available only for the vLLM backend." - ) - seed = 0 temperature = 0.0 max_tokens = 16 @@ -397,13 +380,8 @@ async def test_chat_streaming_usage_option( @pytest.mark.asyncio async def test_completion_streaming_usage_option( - self, client: openai.AsyncOpenAI, model: str, prompt: str, backend: str + self, client: openai.AsyncOpenAI, model: str, prompt: str ): - if backend != "vllm": - pytest.skip( - "Usage reporting is currently available only for the vLLM backend." - ) - seed = 0 temperature = 0.0 max_tokens = 16 @@ -509,36 +487,3 @@ async def test_stream_options_without_streaming( stream_options={"include_usage": True}, ) assert "`stream_options` can only be used when `stream` is True" in str(e.value) - - @pytest.mark.asyncio - async def test_streaming_usage_unsupported_backend( - self, client: openai.AsyncOpenAI, model: str, messages: List[dict], backend: str - ): - if backend == "vllm": - pytest.skip( - "This test is for backends that do not support usage reporting." - ) - - with pytest.raises(openai.BadRequestError) as e: - await client.completions.create( - model=model, - prompt="Test prompt", - stream=True, - stream_options={"include_usage": True}, - ) - assert ( - "`stream_options.include_usage` is currently only supported for the vLLM backend" - in str(e.value) - ) - - with pytest.raises(openai.BadRequestError) as e: - await client.chat.completions.create( - model=model, - messages=messages, - stream=True, - stream_options={"include_usage": True}, - ) - assert ( - "`stream_options.include_usage` is currently only supported for the vLLM backend" - in str(e.value) - ) diff --git a/qa/L0_openai/test.sh b/qa/L0_openai/test.sh index 9e098a4140..8083c50444 100755 --- a/qa/L0_openai/test.sh +++ b/qa/L0_openai/test.sh @@ -27,6 +27,17 @@ ### Helpers ### +function download_tensorrt_llm_models { + TENSORRTLLM_VERSION="$1" + TENSORRTLLM_DIR="$2" + rm -rf ${TENSORRTLLM_DIR} && mkdir ${TENSORRTLLM_DIR} + git clone --filter=blob:none --no-checkout https://github.com/NVIDIA/TensorRT-LLM.git ${TENSORRTLLM_DIR} + pushd ${TENSORRTLLM_DIR} + git sparse-checkout set triton_backend/all_models + git checkout ${TENSORRTLLM_VERSION} + popd +} + function install_deps() { # Install python bindings for tritonserver and tritonfrontend # pip install /opt/tritonserver/python/triton*.whl @@ -38,8 +49,13 @@ function install_deps() { pip install -r requirements-test.txt if [ "${IMAGE_KIND}" == "TRTLLM" ]; then - prepare_tensorrtllm meta-llama/Meta-Llama-3.1-8B-Instruct tests/tensorrtllm_models /tmp/engines/llama/3.1-8b-instruct/ - prepare_tensorrtllm mistralai/Mistral-Nemo-Instruct-2407 tests/tensorrtllm_mistral_models /tmp/engines/mistral/nemo-instruct-2407/ + # TODO: Remove this when the next stable version of TRT-LLM is available + TENSORRTLLM_DIR="/workspace/TensorRT-LLM" + TENSORRTLLM_VERSION="v1.2.0rc2" + download_tensorrt_llm_models ${TENSORRTLLM_VERSION} ${TENSORRTLLM_DIR} + + prepare_tensorrtllm meta-llama/Meta-Llama-3.1-8B-Instruct tests/tensorrtllm_models /tmp/engines/llama/3.1-8b-instruct/ ${TENSORRTLLM_DIR} + prepare_tensorrtllm mistralai/Mistral-Nemo-Instruct-2407 tests/tensorrtllm_mistral_models /tmp/engines/mistral/nemo-instruct-2407/ ${TENSORRTLLM_DIR} else prepare_vllm fi @@ -57,9 +73,10 @@ function prepare_tensorrtllm() { MODEL="$1" MODEL_REPO="$2" ENGINE_PATH="$3" + TENSORRTLLM_DIR="$4" mkdir -p ${MODEL_REPO} - cp /app/all_models/inflight_batcher_llm/* "${MODEL_REPO}" -r + cp ${TENSORRTLLM_DIR}/triton_backend/all_models/inflight_batcher_llm/* "${MODEL_REPO}" -r # Ensemble model is not needed for the test rm -rf ${MODEL_REPO}/ensemble @@ -70,8 +87,8 @@ function prepare_tensorrtllm() { FILL_TEMPLATE="/app/tools/fill_template.py" python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/preprocessing/config.pbtxt tokenizer_dir:${ENGINE_PATH},triton_max_batch_size:64,preprocessing_instance_count:1,max_queue_size:0 python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/postprocessing/config.pbtxt tokenizer_dir:${ENGINE_PATH},triton_max_batch_size:64,postprocessing_instance_count:1 - python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/tensorrt_llm_bls/config.pbtxt triton_max_batch_size:64,decoupled_mode:True,bls_instance_count:1,accumulate_tokens:False,logits_datatype:TYPE_FP32 - python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/tensorrt_llm/config.pbtxt triton_backend:tensorrtllm,triton_max_batch_size:64,decoupled_mode:True,max_beam_width:1,engine_dir:${ENGINE_PATH},batching_strategy:inflight_fused_batching,max_queue_size:0,max_queue_delay_microseconds:1000,encoder_input_features_data_type:TYPE_FP16,logits_datatype:TYPE_FP32,exclude_input_in_output:True + python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/tensorrt_llm_bls/config.pbtxt triton_max_batch_size:64,decoupled_mode:True,bls_instance_count:1,accumulate_tokens:False,logits_datatype:TYPE_FP32,prompt_embedding_table_data_type:TYPE_FP16 + python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/tensorrt_llm/config.pbtxt triton_backend:tensorrtllm,triton_max_batch_size:64,decoupled_mode:True,max_beam_width:1,engine_dir:${ENGINE_PATH},batching_strategy:inflight_fused_batching,max_queue_size:0,max_queue_delay_microseconds:1000,encoder_input_features_data_type:TYPE_FP16,logits_datatype:TYPE_FP32,exclude_input_in_output:True,prompt_embedding_table_data_type:TYPE_FP16 } function pre_test() { From 98293ec27344b99f3b4989f12c7cad0eb6113e7e Mon Sep 17 00:00:00 2001 From: J Wyman Date: Thu, 13 Nov 2025 17:04:00 -0500 Subject: [PATCH 103/230] maintenance: Upgrade 'starlette' Python Package (#8506) This change upgrades the 'starlette' python backage used by Triton Server's OpenAI server. In order to upgrade 'starlette' the 'fastapi' package was required to upgraded as well due the diamond dependency they have. 'fastapi': 0.115.6 => 0.121.2 'starlette': 0.40.0 => 0.49.1 --- python/openai/requirements.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/python/openai/requirements.txt b/python/openai/requirements.txt index 608f3b5ac3..cf63446574 100644 --- a/python/openai/requirements.txt +++ b/python/openai/requirements.txt @@ -25,12 +25,13 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # FastAPI Application -fastapi==0.115.6 +fastapi==0.121.2 # Fix httpx version to avoid bug in openai library: # https://community.openai.com/t/error-with-openai-1-56-0-client-init-got-an-unexpected-keyword-argument-proxies/1040332/3 httpx==0.27.2 openai==1.107.3 partial-json-parser # used for parsing partial JSON outputs -# Minimum starlette version needed to address CVE: +# Minimum starlette version needed to address CVE(s): # https://github.com/advisories/GHSA-f96h-pmfr-66vw -starlette>=0.40.0 +# https://github.com/advisories/GHSA-7f5h-v6xp-fcq8 +starlette>=0.49.1 From 23c53dc02d923536dd0b93f0994a0746e7cf6878 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Fri, 14 Nov 2025 15:00:30 -0800 Subject: [PATCH 104/230] Add Model Analyzer to SDK container (#8511) --- Dockerfile.sdk | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dockerfile.sdk b/Dockerfile.sdk index 1804041101..169bd73a24 100644 --- a/Dockerfile.sdk +++ b/Dockerfile.sdk @@ -219,6 +219,11 @@ RUN if [ "$TRITON_ENABLE_GPU" = "ON" ]; then \ RUN rm -f /usr/bin/python && \ ln -s /usr/bin/python3 /usr/bin/python +# Install Model Analyzer +ARG TRITON_MODEL_ANALYZER_REPO_TAG +ARG TRITON_MODEL_ANALYZER_REPO="${TRITON_REPO_ORGANIZATION}/model_analyzer@${TRITON_MODEL_ANALYZER_REPO_TAG}" +RUN pip3 install "git+${TRITON_MODEL_ANALYZER_REPO}" + # Entrypoint Banner ENV NVIDIA_PRODUCT_NAME="Triton Server SDK" COPY docker/entrypoint.d/ /opt/nvidia/entrypoint.d/ From dd995926d12d769061721450aa0dc3841113e4b8 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Tue, 18 Nov 2025 09:13:24 -0800 Subject: [PATCH 105/230] tensorrtllm_backend: build with CUDA 13 (#8519) --- build.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/build.py b/build.py index a662afd21c..5259971c40 100755 --- a/build.py +++ b/build.py @@ -1508,10 +1508,7 @@ def dockerfile_prepare_container_linux(argmap, backends, enable_gpu, target_mach ENV PYTHONPATH=/opt/tritonserver/backends/dali/wheel/dali:$PYTHONPATH """ - if ( - target_platform() not in ["igpu", "windows", "rhel"] - and "tensorrtllm" not in backends - ): + if target_platform() not in ["igpu", "windows", "rhel"]: repo_arch = "sbsa" if target_machine == "aarch64" else "x86_64" df += f""" RUN curl -o /tmp/cuda-keyring.deb \\ From 116c3b45b1aba14010d839a82d3ed434c4f58a90 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Wed, 19 Nov 2025 12:29:06 -0800 Subject: [PATCH 106/230] feat: Support "echo" for TensorRT-LLM models in OpenAI API frontend v1/completions (#8523) --- .../openai_frontend/engine/triton_engine.py | 20 ++++++++++++++++++- .../openai_frontend/engine/utils/triton.py | 11 ++++++---- python/openai/tests/test_completions.py | 13 ++++++++++++ python/openai/tests/test_openai_client.py | 11 +++------- 4 files changed, 42 insertions(+), 13 deletions(-) diff --git a/python/openai/openai_frontend/engine/triton_engine.py b/python/openai/openai_frontend/engine/triton_engine.py index 6a96c5652d..7310a674cc 100644 --- a/python/openai/openai_frontend/engine/triton_engine.py +++ b/python/openai/openai_frontend/engine/triton_engine.py @@ -105,6 +105,8 @@ class TritonModelMetadata: tokenizer: Optional[Any] # LoRA names supported by the backend lora_names: Optional[List[str]] + # Name of the input tensor enabling "echo" parameter in /v1/completions endpoint + echo_tensor_name: Optional[str] # Time that model was loaded by Triton create_time: int # Conversion format between OpenAI and Triton requests @@ -202,7 +204,12 @@ async def chat( # Convert to Triton request format and perform inference responses = metadata.model.async_infer( metadata.inference_request_converter( - metadata.model, prompt, request, lora_name, self.default_max_tokens + metadata.model, + prompt, + request, + lora_name, + metadata.echo_tensor_name, + self.default_max_tokens, ) ) @@ -330,6 +337,7 @@ async def completion( request.prompt, request, lora_name, + metadata.echo_tensor_name, self.default_max_tokens, ) ) @@ -484,12 +492,22 @@ def _get_model_metadata(self) -> Dict[str, TritonModelMetadata]: self.server.options.model_repository, name, model.version ) + echo_tensor_name = None + for input in model.config()["input"]: + if input["name"] in [ + "exclude_input_in_output", + "sampling_param_exclude_input_from_output", + ]: + echo_tensor_name = input["name"] + break + metadata = TritonModelMetadata( name=name, backend=backend, model=model, tokenizer=self.tokenizer, lora_names=lora_names, + echo_tensor_name=echo_tensor_name, create_time=self.create_time, inference_request_converter=self._determine_request_converter( backend, RequestKind.GENERATION diff --git a/python/openai/openai_frontend/engine/utils/triton.py b/python/openai/openai_frontend/engine/utils/triton.py index 2880875a9e..00cc0c160e 100644 --- a/python/openai/openai_frontend/engine/utils/triton.py +++ b/python/openai/openai_frontend/engine/utils/triton.py @@ -57,6 +57,7 @@ def _create_vllm_generate_request( prompt, request: CreateChatCompletionRequest | CreateCompletionRequest, lora_name: str | None, + echo_tensor_name: str | None, default_max_tokens: int, ): inputs = {} @@ -128,7 +129,7 @@ def _create_vllm_generate_request( inputs["text_input"] = [prompt] inputs["stream"] = np.bool_([request.stream]) - inputs["exclude_input_in_output"] = np.bool_([exclude_input_in_output]) + inputs[echo_tensor_name] = np.bool_([exclude_input_in_output]) # Pass sampling_parameters as serialized JSON string input to support List # fields like 'stop' that aren't supported by TRITONSERVER_Parameters yet. inputs["sampling_parameters"] = [sampling_parameters] @@ -142,6 +143,7 @@ def _create_trtllm_generate_request( prompt, request: CreateChatCompletionRequest | CreateCompletionRequest, lora_name: str | None, + echo_tensor_name: str | None, default_max_tokens: int, ): if lora_name is not None: @@ -184,6 +186,10 @@ def _create_trtllm_generate_request( inputs["seed"] = np.uint64([[request.seed]]) if request.temperature is not None: inputs["temperature"] = np.float32([[request.temperature]]) + # Only limited TRT-LLM models support "echo" (inflight_batcher_llm, disaggregated_serving, llmapi) + echo = getattr(request, "echo", None) + if echo is not None and echo_tensor_name is not None: + inputs[echo_tensor_name] = np.bool_([[not echo]]) guided_json = _get_guided_json_from_tool(request) if guided_json is not None: @@ -192,9 +198,6 @@ def _create_trtllm_generate_request( inputs["return_num_input_tokens"] = np.bool_([[True]]) inputs["return_num_output_tokens"] = np.bool_([[True]]) - - # FIXME: TRT-LLM doesn't currently support runtime changes of 'echo' and it - # is configured at model load time, so we don't handle it here for now. return model.create_request(inputs=inputs) diff --git a/python/openai/tests/test_completions.py b/python/openai/tests/test_completions.py index fca7521ad8..3c88a68acd 100644 --- a/python/openai/tests/test_completions.py +++ b/python/openai/tests/test_completions.py @@ -367,6 +367,19 @@ def test_lora(self): def test_multi_lora(self): pass + @pytest.mark.parametrize("echo", [False, True]) + def test_echo(self, client, model: str, prompt: str, echo: bool): + response = client.post( + "/v1/completions", json={"model": model, "prompt": prompt, "echo": echo} + ) + + response_text = response.json()["choices"][0]["text"].strip() + if echo: + assert response_text.startswith(prompt) + else: + # TODO: Consider using a different prompt. In TRT-LLM model, the second response may contain the prompt in the middle of the response even if echo is False, e.g. " Briefly explained.\nWhat is machine learning? She learns from data\nmachine learning". + assert prompt not in response_text + def test_usage_response(self, client, model: str, prompt: str): response = client.post( "/v1/completions", diff --git a/python/openai/tests/test_openai_client.py b/python/openai/tests/test_openai_client.py index 5ffcbe4f1d..0c02f69db3 100644 --- a/python/openai/tests/test_openai_client.py +++ b/python/openai/tests/test_openai_client.py @@ -92,20 +92,15 @@ def test_openai_client_chat_completion( @pytest.mark.parametrize("echo", [False, True]) def test_openai_client_completion_echo( - self, client: openai.OpenAI, echo: bool, backend: str, model: str, prompt: str + self, client: openai.OpenAI, echo: bool, model: str, prompt: str ): - if backend == "tensorrtllm": - pytest.skip( - reason="TRT-LLM backend currently only supports setting this parameter at model load time", - ) - completion = client.completions.create(prompt=prompt, model=model, echo=echo) - print(f"Completion results: {completion}") response = completion.choices[0].text if echo: - assert prompt in response + assert response.startswith(prompt) else: + # TODO: Consider using a different prompt. In TRT-LLM model, the second response may contain the prompt in the middle of the response even if echo is False, e.g. " Briefly explained.\nWhat is machine learning? She learns from data\nmachine learning". assert prompt not in response @pytest.mark.skip(reason="Not Implemented Yet") From db6aa4e2d96c13d6662c81bf1cfce20fc3ec207e Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Wed, 19 Nov 2025 14:30:11 -0800 Subject: [PATCH 107/230] =?UTF-8?q?Update=20'build.py'=20to=20allow=20cust?= =?UTF-8?q?omer=20set=20the=20dependency=20group=20for=20wheel=20=E2=80=A6?= =?UTF-8?q?=20(#8525)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/build.py b/build.py index 5259971c40..950bf48968 100755 --- a/build.py +++ b/build.py @@ -1240,16 +1240,16 @@ def create_dockerfile_linux( argmap, backends, FLAGS.enable_gpu, target_machine() ) - df += """ + df += f""" WORKDIR /opt COPY --chown=1000:1000 build/install tritonserver WORKDIR /opt/tritonserver COPY --chown=1000:1000 NVIDIA_Deep_Learning_Container_License.pdf . RUN find /opt/tritonserver/python -maxdepth 1 -type f -name \\ - "tritonserver-*.whl" | xargs -I {} pip install --upgrade {}[all] && \\ + "tritonserver-*.whl" | xargs -I {{}} pip install --upgrade {{}}[{FLAGS.triton_wheels_dependencies_group}] && \\ find /opt/tritonserver/python -maxdepth 1 -type f -name \\ - "tritonfrontend-*.whl" | xargs -I {} pip install --upgrade {}[all] + "tritonfrontend-*.whl" | xargs -I {{}} pip install --upgrade {{}}[{FLAGS.triton_wheels_dependencies_group}] RUN pip3 install -r python/openai/requirements.txt @@ -2784,6 +2784,13 @@ def enable_all(): " - 'build_public_vllm': A flag (default is 'true') indicating whether to build the public VLLM version.\n\n" "Ensure that the required environment variables for these secrets are set before running the build.", ) + parser.add_argument( + "--triton-wheels-dependencies-group", + required=False, + type=str, + default="all", + help="The group of dependencies for Triton wheels to be installed. Default value is 'all'.", + ) FLAGS = parser.parse_args() if FLAGS.image is None: From d8704c49393fef35808e5579869c1103b827f40b Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Thu, 20 Nov 2025 08:57:12 -0800 Subject: [PATCH 108/230] Update L0_python_backend test scenario (#8526) --- qa/L0_backend_python/common.sh | 23 +++++++++++++------ qa/L0_backend_python/env/test.sh | 2 +- .../setup_python_enviroment.sh | 4 ++-- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/qa/L0_backend_python/common.sh b/qa/L0_backend_python/common.sh index e23fd6e95b..325ddc34eb 100755 --- a/qa/L0_backend_python/common.sh +++ b/qa/L0_backend_python/common.sh @@ -87,11 +87,20 @@ create_python_backend_stub() { git clone ${TRITON_REPO_ORGANIZATION}/python_backend -b $PYTHON_BACKEND_REPO_TAG CUDA_PATH=$(readlink -f /usr/local/cuda) export CMAKE_POLICY_VERSION_MINIMUM=3.5 - (cd python_backend/ && mkdir builddir && cd builddir && \ - export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ - cmake -DTRITON_ENABLE_GPU=ON -DCMAKE_CUDA_COMPILER=$CUDA_PATH/bin/nvcc \ - -DCUDAToolkit_ROOT=$CUDA_PATH -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ - -DTRITON_BACKEND_REPO_TAG=$TRITON_BACKEND_REPO_TAG -DTRITON_COMMON_REPO_TAG=$TRITON_COMMON_REPO_TAG \ - -DTRITON_CORE_REPO_TAG=$TRITON_CORE_REPO_TAG -DPYBIND11_PYTHON_VERSION=$PY_VERSION ../ && \ - make -j18 triton-python-backend-stub) + (cd python_backend/ \ + && mkdir builddir \ + && cd builddir \ + && export CMAKE_POLICY_VERSION_MINIMUM=3.5 \ + && cmake \ + -DCMAKE_CUDA_COMPILER=$CUDA_PATH/bin/nvcc \ + -DCMAKE_INCLUDE_PATH:STRING=/usr/include \ + -DCUDAToolkit_ROOT=$CUDA_PATH \ + -DPYBIND11_PYTHON_VERSION=$PY_VERSION \ + -DTRITON_BACKEND_REPO_TAG=$TRITON_BACKEND_REPO_TAG \ + -DTRITON_COMMON_REPO_TAG=$TRITON_COMMON_REPO_TAG \ + -DTRITON_CORE_REPO_TAG=$TRITON_CORE_REPO_TAG \ + -DTRITON_ENABLE_GPU=ON \ + -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ + -S ../ \ + && cmake --build . --target triton-python-backend-stub -j18) } diff --git a/qa/L0_backend_python/env/test.sh b/qa/L0_backend_python/env/test.sh index 91870cbff3..67d543c762 100755 --- a/qa/L0_backend_python/env/test.sh +++ b/qa/L0_backend_python/env/test.sh @@ -48,7 +48,7 @@ install_conda path_to_conda_pack='$$TRITON_MODEL_DIRECTORY/python_3_12_environment.tar.gz' create_conda_env "3.12" "python-3-12" conda install -c conda-forge libstdcxx-ng=14 -y -TORCH_VERSION="2.6.0" +TORCH_VERSION="2.8.0" conda install numpy=1.26.4 -y if [ $TRITON_RHEL -eq 1 ]; then TORCH_VERISON="2.17.0" diff --git a/qa/L0_backend_python/setup_python_enviroment.sh b/qa/L0_backend_python/setup_python_enviroment.sh index 97901e1bb6..ea93cd551b 100755 --- a/qa/L0_backend_python/setup_python_enviroment.sh +++ b/qa/L0_backend_python/setup_python_enviroment.sh @@ -56,10 +56,10 @@ conda update -n base -c defaults conda -y # been setup correctly. if [ ${PYTHON_ENV_VERSION} = "11" ]; then create_conda_env "3.11" "python-3-11" - conda install pytorch=2.6.0 -y + conda install pytorch=2.8.0 -y conda install -c conda-forge libstdcxx-ng=14 -y conda install numpy=1.23.5 -y - EXPECTED_VERSION_STRING="Python version is 3.11, NumPy version is 1.23.5, and PyTorch version is 2.6.0" + EXPECTED_VERSION_STRING="Python version is 3.11, NumPy version is 1.23.5, and PyTorch version is 2.8.0" create_python_backend_stub conda-pack -o python3.11.tar.gz path_to_conda_pack="$PWD/python-3-11" From 15010d2e27489ad787296746c8782632346c9498 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:25:00 -0800 Subject: [PATCH 109/230] models: restructure models generation script. (#8544) --- docs/customization_guide/test.md | 3 +- qa/common/gen_qa_custom_ops | 149 ------- qa/common/gen_qa_model_repository | 669 +++++++++++++++--------------- 3 files changed, 346 insertions(+), 475 deletions(-) delete mode 100755 qa/common/gen_qa_custom_ops diff --git a/docs/customization_guide/test.md b/docs/customization_guide/test.md index a85a10f48b..d1418aadf9 100644 --- a/docs/customization_guide/test.md +++ b/docs/customization_guide/test.md @@ -1,5 +1,5 @@ -# [Triton Inference Server Release 25.10](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-25-10.html#rel-25-10) +# [Triton Inference Server Release 25.11](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-25-10.html#rel-25-10) -The Triton Inference Server container image, release 25.10, is available +The Triton Inference Server container image, release 25.11, is available on [NGC](https://ngc.nvidia.com/catalog/containers/nvidia:tritonserver) and is open source on [GitHub](https://github.com/triton-inference-server/server). Release notes can diff --git a/python/openai/README.md b/python/openai/README.md index a609946493..71244b373e 100644 --- a/python/openai/README.md +++ b/python/openai/README.md @@ -51,7 +51,7 @@ docker run -it --net=host --gpus all --rm \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ -e HF_TOKEN \ - nvcr.io/nvidia/tritonserver:25.10-vllm-python-py3 + nvcr.io/nvidia/tritonserver:25.11-vllm-python-py3 ``` 2. Launch the OpenAI-compatible Triton Inference Server: @@ -313,7 +313,7 @@ Currently, OpenAI-Compatible Frontend supports loading embedding models and embe docker run -it --net=host --gpus all --rm \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ -e HF_TOKEN \ - nvcr.io/nvidia/tritonserver:25.10-vllm-python-py3 + nvcr.io/nvidia/tritonserver:25.11-vllm-python-py3 ``` 2. Launch the OpenAI-compatible Triton Inference Server: @@ -610,10 +610,9 @@ tool calling result: The weather in Dallas, Texas is 85 degrees fahrenheit. It i #### Named Tool Calling -The OpenAI frontend supports named function calling, utilizing guided decoding in the vLLM and TensorRT-LLM backends. Users can specify one of the tools in `tool_choice` to force the model to select a specific tool for function calling. +The OpenAI frontend supports named function calling, utilizing structured outputs in the vLLM backend and guided decoding in TensorRT-LLM backend. Users can specify one of the tools in `tool_choice` to force the model to select a specific tool for function calling. > [!NOTE] -> The latest release of TensorRT-LLM (v0.18.0) does not yet support guided decoding. To enable this feature, use a build from the main branch of TensorRT-LLM. > For instructions on enabling guided decoding in the TensorRT-LLM backend, please refer to [this guide](https://github.com/triton-inference-server/tensorrtllm_backend/blob/main/docs/guided_decoding.md) Example for making a named tool calling request: diff --git a/python/openai/openai_frontend/engine/triton_engine.py b/python/openai/openai_frontend/engine/triton_engine.py index 7310a674cc..268a90c4b9 100644 --- a/python/openai/openai_frontend/engine/triton_engine.py +++ b/python/openai/openai_frontend/engine/triton_engine.py @@ -696,7 +696,8 @@ def _get_streaming_response_delta( # check to make sure we haven't "forgotten" to stream # any tokens that were generated but previously # matched by partial json parsing, such as '}'. - # only happens if we are NOT using guided decoding + # only happens if we are NOT using structured outputs + # or guided decoding if ( self._should_check_for_unstreamed_tool_arg_tokens( response_delta=response_delta, diff --git a/python/openai/openai_frontend/engine/utils/triton.py b/python/openai/openai_frontend/engine/utils/triton.py index 00cc0c160e..545d9a72db 100644 --- a/python/openai/openai_frontend/engine/utils/triton.py +++ b/python/openai/openai_frontend/engine/utils/triton.py @@ -110,17 +110,15 @@ def _create_vllm_generate_request( if lora_name is not None: sampling_parameters["lora_name"] = lora_name - sampling_parameters = json.dumps(sampling_parameters) guided_json = _get_guided_json_from_tool(request) if guided_json is not None: - from vllm.sampling_params import GuidedDecodingParams + from vllm.sampling_params import StructuredOutputsParams - sampling_parameters_json = json.loads(sampling_parameters) - sampling_parameters_json["guided_decoding"] = json.dumps( - asdict(GuidedDecodingParams.from_optional(json=guided_json)) + sampling_parameters["structured_outputs"] = json.dumps( + asdict(StructuredOutputsParams(json=guided_json)) ) - sampling_parameters = json.dumps(sampling_parameters_json) + sampling_parameters = json.dumps(sampling_parameters) exclude_input_in_output = True echo = getattr(request, "echo", None) @@ -382,13 +380,12 @@ def _validate_triton_responses_non_streaming( responses: List[tritonserver._api._response.InferenceResponse], ): num_responses = len(responses) - if num_responses == 1 and responses[0].final != True: - raise ServerError("Unexpected internal error with incorrect response flags") - if num_responses == 2 and responses[-1].final != True: - raise ServerError("Unexpected internal error with incorrect response flags") - if num_responses > 2: + if 1 <= num_responses <= 2: + if responses[-1].final != True: + raise ServerError("Unexpected internal error with incorrect response flags") + else: raise ServerError( - f"Unexpected number of responses: {num_responses}, expected 1." + f"Unexpected number of responses: {num_responses}, expected 1 or 2." ) diff --git a/python/openai/tests/conftest.py b/python/openai/tests/conftest.py index 5c269a78c1..cff670f3ec 100644 --- a/python/openai/tests/conftest.py +++ b/python/openai/tests/conftest.py @@ -32,6 +32,13 @@ from tests.utils import OpenAIServer, setup_fastapi_app, setup_server +def pytest_configure(config): + """Register custom markers.""" + config.addinivalue_line( + "markers", "openai: mark test to run with OpenAI server (subprocess)" + ) + + ### TEST ENVIRONMENT SETUP ### def infer_test_environment(tool_call_parser): # Infer the test environment for simplicity in local dev/testing. diff --git a/python/openai/tests/test_chat_completions.py b/python/openai/tests/test_chat_completions.py index 2b410c3cc2..4034def2cd 100644 --- a/python/openai/tests/test_chat_completions.py +++ b/python/openai/tests/test_chat_completions.py @@ -590,7 +590,7 @@ def test_chat_completions_custom_tokenizer( # Tokenizers can be provided by a local file path to a directory containing # the relevant files such as tokenizer.json and tokenizer_config.json. custom_tokenizer_path = str(Path(__file__).parent / "custom_tokenizer") - download_cmd = f"huggingface-cli download --local-dir {custom_tokenizer_path} {tokenizer_model} --include *.json" + download_cmd = f"hf download --local-dir {custom_tokenizer_path} {tokenizer_model} --include *.json" print(f"Running download command: {download_cmd}") subprocess.run(download_cmd.split(), check=True) diff --git a/python/openai/tests/test_tool_calling.py b/python/openai/tests/test_tool_calling.py index c738b96303..8f794cc62c 100644 --- a/python/openai/tests/test_tool_calling.py +++ b/python/openai/tests/test_tool_calling.py @@ -341,10 +341,6 @@ async def test_tool_call_with_reply_response( # validate if steaming and non-streaming generates the same content assert "".join(chunks) == choice.message.content - @pytest.mark.skipif( - os.environ.get("IMAGE_KIND") == "TRTLLM", - reason="latest release version of Tensorrt LLM 0.18 doesn't support guided decoding", - ) @pytest.mark.asyncio async def test_tool_call_with_named_tool_choice( self, client: openai.AsyncOpenAI, model: str @@ -448,10 +444,6 @@ async def test_tool_call_with_named_tool_choice( assert choice.message.role == role_name assert choice.message.tool_calls[0].function.name == function_name - @pytest.mark.skipif( - os.environ.get("IMAGE_KIND") == "TRTLLM", - reason="latest release version of Tensorrt LLM 0.18 doesn't support guided decoding", - ) @pytest.mark.asyncio async def test_tool_call_with_required_tool_choice( self, client: openai.AsyncOpenAI, model: str diff --git a/qa/L0_openai/test.sh b/qa/L0_openai/test.sh index 8083c50444..a1db293436 100755 --- a/qa/L0_openai/test.sh +++ b/qa/L0_openai/test.sh @@ -74,6 +74,9 @@ function prepare_tensorrtllm() { MODEL_REPO="$2" ENGINE_PATH="$3" TENSORRTLLM_DIR="$4" + TRITON_BACKEND=tensorrtllm + XGRAMMAR_TOKENIZER_INFO_PATH=tokenizer_info/${MODEL}/xgrammar_tokenizer_info.json + GUIDED_DECODING_BACKEND=xgrammar mkdir -p ${MODEL_REPO} cp ${TENSORRTLLM_DIR}/triton_backend/all_models/inflight_batcher_llm/* "${MODEL_REPO}" -r @@ -83,12 +86,17 @@ function prepare_tensorrtllm() { # 1. Generate the model's trt engines python3 ../generate_engine.py --model "${MODEL}" --engine_path "${ENGINE_PATH}" - # 2. Prepare model repository + # 2. Generate the model's xgrammar tokenizer info. In order to run on C++ backend, we need an extra step to extract tokenizer’s information into json format. + XGRAMMAR_TOKENIZER_INFO_DIR=tokenizer_info/${MODEL} + rm -rf ${XGRAMMAR_TOKENIZER_INFO_DIR} + python3 /app/examples/generate_xgrammar_tokenizer_info.py --model_dir ${MODEL} --output_dir ${XGRAMMAR_TOKENIZER_INFO_DIR} + + # 3. Prepare model repository FILL_TEMPLATE="/app/tools/fill_template.py" python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/preprocessing/config.pbtxt tokenizer_dir:${ENGINE_PATH},triton_max_batch_size:64,preprocessing_instance_count:1,max_queue_size:0 python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/postprocessing/config.pbtxt tokenizer_dir:${ENGINE_PATH},triton_max_batch_size:64,postprocessing_instance_count:1 python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/tensorrt_llm_bls/config.pbtxt triton_max_batch_size:64,decoupled_mode:True,bls_instance_count:1,accumulate_tokens:False,logits_datatype:TYPE_FP32,prompt_embedding_table_data_type:TYPE_FP16 - python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/tensorrt_llm/config.pbtxt triton_backend:tensorrtllm,triton_max_batch_size:64,decoupled_mode:True,max_beam_width:1,engine_dir:${ENGINE_PATH},batching_strategy:inflight_fused_batching,max_queue_size:0,max_queue_delay_microseconds:1000,encoder_input_features_data_type:TYPE_FP16,logits_datatype:TYPE_FP32,exclude_input_in_output:True,prompt_embedding_table_data_type:TYPE_FP16 + python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/tensorrt_llm/config.pbtxt triton_backend:${TRITON_BACKEND},triton_max_batch_size:64,decoupled_mode:True,max_beam_width:1,engine_dir:${ENGINE_PATH},batching_strategy:inflight_fused_batching,max_queue_size:0,max_queue_delay_microseconds:1000,encoder_input_features_data_type:TYPE_FP16,logits_datatype:TYPE_FP32,exclude_input_in_output:True,prompt_embedding_table_data_type:TYPE_FP16,guided_decoding_backend:${GUIDED_DECODING_BACKEND},xgrammar_tokenizer_info_path:${XGRAMMAR_TOKENIZER_INFO_PATH} } function pre_test() { diff --git a/qa/common/gen_jetson_trt_models b/qa/common/gen_jetson_trt_models index 693d233f91..b763d2c490 100755 --- a/qa/common/gen_jetson_trt_models +++ b/qa/common/gen_jetson_trt_models @@ -34,7 +34,7 @@ # Make all generated files accessible outside of container umask 0000 # Set the version of the models -TRITON_VERSION=${TRITON_VERSION:=25.10} +TRITON_VERSION=${TRITON_VERSION:=25.11} # Set the CUDA device to use CUDA_DEVICE=${RUNNER_ID:=0} # Set TensorRT image diff --git a/qa/common/gen_qa_model_repository b/qa/common/gen_qa_model_repository index ce321ee11b..5b59beb715 100755 --- a/qa/common/gen_qa_model_repository +++ b/qa/common/gen_qa_model_repository @@ -60,7 +60,7 @@ log_message.status "Changing working directory to the script directory to: " "${ cd ${TRITON_MDLS_BASE_SCRIPT_DIR} log_message.status "define: default values" -TRITON_VERSION=${TRITON_VERSION:=25.10} +TRITON_VERSION=${TRITON_VERSION:=25.11} ONNX_VERSION=1.16.1 ONNX_OPSET=0 OPENVINO_VERSION=2024.5.0 @@ -325,10 +325,10 @@ chmod -R 777 $TRITON_MDLS_QA_TRT_FORMAT_MODEL nvidia-smi --query-gpu=compute_cap | grep -qz 11.0 && echo -e '\033[33m[WARNING]\033[0m Skipping model generation for data dependent shape' || python3 $TRITON_MDLS_SRC_DIR/gen_qa_trt_data_dependent_shape.py --models_dir=$TRITON_MDLS_QA_TRT_DATA_DEPENDENT_MODEL chmod -R 777 $TRITON_MDLS_QA_TRT_DATA_DEPENDENT_MODEL # Make shared library for custom Hardmax plugin. -if [ -d "/usr/src/tensorrt" ]; then +if [ -d "/usr/src/tensorrt/samples/python/onnx_custom_plugin" ]; then cd /usr/src/tensorrt/samples/python/onnx_custom_plugin else - git clone -b release/\${TRT_VERSION%.*.*} --depth 1 https://github.com/NVIDIA/TensorRT.git + git clone -b release/\${TRT_VERSION%.*.*} --depth 1 https://github.com/NVIDIA/TensorRT.git /workspace/TensorRT cd /workspace/TensorRT/samples/python/onnx_custom_plugin fi rm -rf build && mkdir build && \ From b68e4602b9a06f95d3e75c21b377b86d7e6d8286 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Tue, 2 Dec 2025 14:26:47 -0800 Subject: [PATCH 111/230] models: Update validation condition (#8556) --- qa/common/gen_qa_model_repository | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qa/common/gen_qa_model_repository b/qa/common/gen_qa_model_repository index 5b59beb715..2edfcf325d 100755 --- a/qa/common/gen_qa_model_repository +++ b/qa/common/gen_qa_model_repository @@ -345,7 +345,7 @@ if [ $? -ne 0 ]; then fi log_message.status "check: engine installation" -if which -s docker ; then +if which docker ; then log_message.info "Docker is installed." if ! docker volume inspect $DOCKER_VOLUME > /dev/null 2>&1; then @@ -498,7 +498,7 @@ if which -s docker ; then docker volume rm $DOCKER_VOLUME fi # [ -z $CI ] -elif which -s enroot ; then +elif which enroot ; then log_message.info "NVIDIA Enroot is installed." ; else log_message.warning "Neither Docker nor NVIDIA Enroot is installed." ; From d107e48f9f4773409a7ff8b035e2870d258ff220 Mon Sep 17 00:00:00 2001 From: Sai Kiran Polisetty Date: Thu, 4 Dec 2025 10:07:40 +0530 Subject: [PATCH 112/230] feat: Support `logprobs` for vLLM models in OpenAI Frontend (#8538) --- .../openai_frontend/engine/triton_engine.py | 81 +++++- .../openai_frontend/engine/utils/triton.py | 190 +++++++++++++- .../openai/openai_frontend/schemas/openai.py | 6 +- python/openai/tests/test_chat_completions.py | 131 +++++++++- python/openai/tests/test_completions.py | 99 +++++++- .../tests/test_models/mock_llm/config.pbtxt | 8 +- python/openai/tests/test_openai_client.py | 232 ++++++++++++++++++ 7 files changed, 720 insertions(+), 27 deletions(-) diff --git a/python/openai/openai_frontend/engine/triton_engine.py b/python/openai/openai_frontend/engine/triton_engine.py index 268a90c4b9..179b963b57 100644 --- a/python/openai/openai_frontend/engine/triton_engine.py +++ b/python/openai/openai_frontend/engine/triton_engine.py @@ -57,6 +57,8 @@ _create_trtllm_generate_request, _create_vllm_embedding_request, _create_vllm_generate_request, + _get_openai_chat_format_logprobs_from_vllm_response, + _get_openai_completion_format_logprobs_from_vllm_response, _get_output, _get_usage_from_response, _get_vllm_lora_names, @@ -66,6 +68,7 @@ from schemas.openai import ( ChatCompletionChoice, ChatCompletionFinishReason, + ChatCompletionLogprobs, ChatCompletionMessageToolCall, ChatCompletionMessageToolCallChunk, ChatCompletionNamedToolChoice, @@ -255,13 +258,22 @@ async def chat( response, metadata.backend, RequestKind.GENERATION ) + # Parse logprobs if requested + logprobs_data = None + if request.logprobs: + openai_logprobs = _get_openai_chat_format_logprobs_from_vllm_response( + response + ) + if openai_logprobs: + logprobs_data = ChatCompletionLogprobs(content=openai_logprobs) + return CreateChatCompletionResponse( id=request_id, choices=[ ChatCompletionChoice( index=0, message=response_message, - logprobs=None, + logprobs=logprobs_data, finish_reason=finish_reason, ) ], @@ -360,10 +372,17 @@ async def completion( response, metadata.backend, RequestKind.GENERATION ) + # Parse logprobs if requested + logprobs_data = None + if request.logprobs is not None and request.logprobs > 0: + logprobs_data = _get_openai_completion_format_logprobs_from_vllm_response( + response + ) + choice = Choice( finish_reason=FinishReason.stop, index=0, - logprobs=None, + logprobs=logprobs_data, text=text, ) return CreateCompletionResponse( @@ -605,6 +624,15 @@ async def _streaming_chat_iterator( ) previous_text = current_text + # Parse logprobs for this chunk if requested + chunk_logprobs = None + if request.logprobs: + openai_logprobs = _get_openai_chat_format_logprobs_from_vllm_response( + response + ) + if openai_logprobs: + chunk_logprobs = ChatCompletionLogprobs(content=openai_logprobs) + # if the response delta is None (e.g. because it was a # "control token" for tool calls or the parser otherwise # wasn't ready to send a token, then @@ -618,7 +646,7 @@ async def _streaming_chat_iterator( choice = ChatCompletionStreamingResponseChoice( index=0, delta=response_delta, - logprobs=None, + logprobs=chunk_logprobs, finish_reason=finish_reason, ) @@ -791,8 +819,19 @@ def _validate_chat_request( f"Received n={request.n}, but only single choice (n=1) is currently supported" ) - if request.logit_bias is not None or request.logprobs: - raise ClientError("logit bias and log probs not currently supported") + if request.logit_bias is not None: + raise ClientError("logit bias is not currently supported") + + # Logprobs are only supported for vLLM backend currently + if metadata.backend != "vllm" and ( + request.logprobs is not None or request.top_logprobs is not None + ): + raise ClientError( + "logprobs are currently available only for the vLLM backend" + ) + + if request.top_logprobs is not None and not request.logprobs: + raise ClientError("`top_logprobs` can only be used when `logprobs` is True") self._verify_chat_tool_call_settings(request=request) @@ -847,16 +886,32 @@ async def _streaming_completion_iterator( model = request.model include_usage = request.stream_options and request.stream_options.include_usage usage_accumulator = _StreamingUsageAccumulator(backend) + current_offset = 0 async for response in responses: if include_usage: usage_accumulator.update(response) text = _get_output(response) + + # Parse logprobs for this chunk if requested + chunk_logprobs = None + if request.logprobs is not None and request.logprobs > 0: + chunk_logprobs = ( + _get_openai_completion_format_logprobs_from_vllm_response(response) + ) + # Adjust text offsets based on accumulated output + if chunk_logprobs and chunk_logprobs.text_offset: + chunk_logprobs.text_offset = [ + offset + current_offset for offset in chunk_logprobs.text_offset + ] + + current_offset += len(text) + choice = Choice( finish_reason=FinishReason.stop if response.final else None, index=0, - logprobs=None, + logprobs=chunk_logprobs, text=text, ) chunk = CreateCompletionResponse( @@ -942,8 +997,18 @@ def _validate_completion_request( f"Received best_of={request.best_of}, but only single choice (best_of=1) is currently supported" ) - if request.logit_bias is not None or request.logprobs is not None: - raise ClientError("logit bias and log probs not supported") + if request.logit_bias is not None: + raise ClientError("logit bias is not supported") + + # Logprobs are only supported for vLLM backend currently + if ( + request.logprobs is not None + and request.logprobs > 0 + and metadata.backend != "vllm" + ): + raise ClientError( + "logprobs are currently available only for the vLLM backend" + ) if request.stream_options and not request.stream: raise ClientError("`stream_options` can only be used when `stream` is True") diff --git a/python/openai/openai_frontend/engine/utils/triton.py b/python/openai/openai_frontend/engine/utils/triton.py index 545d9a72db..e2c4cf92c9 100644 --- a/python/openai/openai_frontend/engine/utils/triton.py +++ b/python/openai/openai_frontend/engine/utils/triton.py @@ -27,22 +27,26 @@ import json import os import re +import sys from dataclasses import asdict, dataclass, field from enum import Enum from pathlib import Path -from typing import Iterable, List, Optional, Union +from typing import Dict, Iterable, List, Optional, Union import numpy as np import tritonserver from pydantic import BaseModel from schemas.openai import ( ChatCompletionNamedToolChoice, + ChatCompletionTokenLogprob, ChatCompletionToolChoiceOption1, CompletionUsage, CreateChatCompletionRequest, CreateCompletionRequest, CreateEmbeddingRequest, EmbeddingUsage, + Logprobs, + TopLogprob, ) from utils.utils import ClientError, ServerError @@ -83,6 +87,8 @@ def _create_vllm_generate_request( "max_completion_tokens", # will be handled explicitly "max_tokens", + "logprobs", + "top_logprobs", } # NOTE: The exclude_none is important, as internals may not support @@ -92,6 +98,7 @@ def _create_vllm_generate_request( exclude_none=True, ) + request_logprobs = False # Indicates CreateChatCompletionRequest if hasattr(request, "max_completion_tokens"): if request.max_completion_tokens is not None: @@ -102,11 +109,31 @@ def _create_vllm_generate_request( # If neither is set, use a default value for max_tokens else: sampling_parameters["max_tokens"] = default_max_tokens + + # Handle logprobs for chat completions + # OpenAI API: logprobs (bool), top_logprobs (int 0-20) + # vLLM API: logprobs (int) - number of top token logprobs to return + if request.logprobs and request.top_logprobs is not None: + sampling_parameters["logprobs"] = request.top_logprobs + request_logprobs = True + elif request.logprobs: + # If logprobs=True but top_logprobs not specified, default to 1 + sampling_parameters["logprobs"] = 1 + request_logprobs = True # Indicates CreateCompletionRequest - elif request.max_tokens is not None: - sampling_parameters["max_tokens"] = request.max_tokens else: - sampling_parameters["max_tokens"] = default_max_tokens + if request.max_tokens is not None: + sampling_parameters["max_tokens"] = request.max_tokens + else: + sampling_parameters["max_tokens"] = default_max_tokens + + # Handle logprobs for completions + # OpenAI API: logprobs (int 0-5) - number of top token log probs + # vLLM API: logprobs (int) - same behavior, pass directly + if request.logprobs is not None and request.logprobs > 0: + sampling_parameters["logprobs"] = request.logprobs + request_logprobs = True + inputs["return_logprobs"] = np.bool_([request_logprobs]) if lora_name is not None: sampling_parameters["lora_name"] = lora_name @@ -376,6 +403,161 @@ def _get_output(response: tritonserver._api._response.InferenceResponse) -> str: return "" +def _get_logprobs_from_response( + response: tritonserver._api._response.InferenceResponse, +) -> Optional[List[Dict]]: + """ + Extracts logprobs from a Triton inference response (vLLM backend). + + Returns: + List of dictionaries containing logprobs data, or None if not available. + Format: [ + { + token_id: { + "logprob": float, + "rank": int, + "decoded_token": str + } + }, + ... + ] + """ + if "logprobs" not in response.outputs: + return None + + logprobs_tensor = response.outputs["logprobs"] + if logprobs_tensor is None: + return None + + # The logprobs are stored as JSON string (vLLM backend) + logprobs_str = _to_string(logprobs_tensor) + + if logprobs_str == "null": + return None + + try: + logprobs_data = json.loads(logprobs_str) + return logprobs_data + except json.JSONDecodeError: + return None + + +def _get_openai_chat_format_logprobs_from_vllm_response( + response: tritonserver._api._response.InferenceResponse, +) -> Optional[List[ChatCompletionTokenLogprob]]: + """ + Convert logprobs from a Triton inference response (vLLM backend) to OpenAI chat completion format. + + Args: + response: Triton inference response containing logprobs output. + + Returns: + List of ChatCompletionTokenLogprob objects, or None if no logprobs available. + """ + vllm_logprobs = _get_logprobs_from_response(response) + + if not vllm_logprobs: + return None + + openai_logprobs = [] + for token_logprobs_dict in vllm_logprobs: + if not token_logprobs_dict: + continue + + # Sort by rank to identify the selected token (rank=1 is always the chosen token) + sorted_tokens = sorted( + token_logprobs_dict.items(), key=lambda x: x[1].get("rank", sys.maxsize) + ) + + # The first token (lowest rank) is the selected token + selected_token_id, selected_token_data = sorted_tokens[0] + selected_token = selected_token_data["decoded_token"] + selected_logprob = selected_token_data["logprob"] + + # Convert to bytes representation + token_bytes = list(selected_token.encode("utf-8")) + + top_logprobs_list = [] + for token_id, token_data in sorted_tokens: + decoded_token = token_data["decoded_token"] + top_logprobs_list.append( + TopLogprob( + token=decoded_token, + logprob=token_data["logprob"], + bytes=list(decoded_token.encode("utf-8")), + ) + ) + + openai_logprobs.append( + ChatCompletionTokenLogprob( + token=selected_token, + logprob=selected_logprob, + bytes=token_bytes, + top_logprobs=top_logprobs_list, + ) + ) + + return openai_logprobs + + +def _get_openai_completion_format_logprobs_from_vllm_response( + response: tritonserver._api._response.InferenceResponse, +) -> Optional[Logprobs]: + """ + Convert logprobs from a Triton inference response (vLLM backend) to OpenAI completion format. + + Args: + response: Triton inference response containing logprobs output. + + Returns: + Logprobs object for completions API, or None if no logprobs available. + """ + vllm_logprobs = _get_logprobs_from_response(response) + + if not vllm_logprobs: + return None + + text_offset = [] + token_logprobs = [] + tokens = [] + top_logprobs = [] + + current_offset = 0 + for token_logprobs_dict in vllm_logprobs: + if not token_logprobs_dict: + continue + + # Sort by rank to identify the selected token (rank=1 is always the chosen token) + sorted_tokens = sorted( + token_logprobs_dict.items(), key=lambda x: x[1].get("rank", sys.maxsize) + ) + + # The first token (lowest rank) is the selected token + selected_token_id, selected_token_data = sorted_tokens[0] + selected_token = selected_token_data["decoded_token"] + selected_logprob = selected_token_data["logprob"] + + text_offset.append(current_offset) + token_logprobs.append(selected_logprob) + tokens.append(selected_token) + + # Build top_logprobs dict for this position + top_logprobs_dict = {} + for token_id, token_data in sorted_tokens: + decoded_token = token_data["decoded_token"] + top_logprobs_dict[decoded_token] = token_data["logprob"] + top_logprobs.append(top_logprobs_dict) + + current_offset += len(selected_token) + + return Logprobs( + text_offset=text_offset, + token_logprobs=token_logprobs, + tokens=tokens, + top_logprobs=top_logprobs, + ) + + def _validate_triton_responses_non_streaming( responses: List[tritonserver._api._response.InferenceResponse], ): diff --git a/python/openai/openai_frontend/schemas/openai.py b/python/openai/openai_frontend/schemas/openai.py index 81ff6e93b3..57fb7b1017 100644 --- a/python/openai/openai_frontend/schemas/openai.py +++ b/python/openai/openai_frontend/schemas/openai.py @@ -530,7 +530,7 @@ class ChatCompletionTokenLogprob(BaseModel): ) -class Logprobs2(BaseModel): +class ChatCompletionLogprobs(BaseModel): content: List[ChatCompletionTokenLogprob] = Field( ..., description="A list of message content tokens with log probability information.", @@ -539,7 +539,7 @@ class Logprobs2(BaseModel): class ChatCompletionStreamingResponseChoice(BaseModel): delta: ChatCompletionStreamResponseDelta - logprobs: Optional[Logprobs2] = Field( + logprobs: Optional[ChatCompletionLogprobs] = Field( None, description="Log probability information for the choice." ) finish_reason: ChatCompletionFinishReason | None = Field( @@ -730,7 +730,7 @@ class ChatCompletionChoice(BaseModel): ..., description="The index of the choice in the list of choices." ) message: ChatCompletionResponseMessage - logprobs: Logprobs2 | None = Field( + logprobs: ChatCompletionLogprobs | None = Field( ..., description="Log probability information for the choice." ) diff --git a/python/openai/tests/test_chat_completions.py b/python/openai/tests/test_chat_completions.py index 4034def2cd..e708689e0e 100644 --- a/python/openai/tests/test_chat_completions.py +++ b/python/openai/tests/test_chat_completions.py @@ -154,13 +154,10 @@ def test_chat_completions_sampling_parameters( ) # FIXME: Add support and remove this check - unsupported_parameters = ["logprobs", "logit_bias"] + unsupported_parameters = ["logit_bias"] if param_key in unsupported_parameters: assert response.status_code == 400 - assert ( - response.json()["detail"] - == "logit bias and log probs not currently supported" - ) + assert response.json()["detail"] == "logit bias is not currently supported" return assert response.status_code == 200 @@ -522,10 +519,6 @@ def test_multi_lora(self): def test_request_n_choices(self): pass - @pytest.mark.skip(reason="Not Implemented Yet") - def test_request_logprobs(self): - pass - @pytest.mark.skip(reason="Not Implemented Yet") def test_request_logit_bias(self): pass @@ -548,6 +541,126 @@ def test_usage_response(self, client, model: str, messages: List[dict]): usage["total_tokens"] == usage["prompt_tokens"] + usage["completion_tokens"] ) + def test_chat_completions_logprobs( + self, client, backend: str, model: str, messages: List[dict] + ): + """Test logprobs parameter for chat completions.""" + response = client.post( + "/v1/chat/completions", + json={ + "model": model, + "messages": messages, + "logprobs": True, + "top_logprobs": 2, + "max_tokens": 10, + }, + ) + + # Non-vLLM backends should raise an error + if backend != "vllm": + assert response.status_code == 400 + assert ( + "logprobs are currently available only for the vLLM backend" + in response.json()["detail"] + ) + return + + assert response.status_code == 200 + response_json = response.json() + + # Check that logprobs are present in the response + choice = response_json["choices"][0] + assert "logprobs" in choice + logprobs = choice["logprobs"] + + assert logprobs is not None + assert "content" in logprobs + content = logprobs["content"] + assert isinstance(content, list) + assert len(content) > 0 + + # Validate structure of each token logprob + for token_logprob in content: + assert "token" in token_logprob + assert "logprob" in token_logprob + assert "bytes" in token_logprob + assert "top_logprobs" in token_logprob + + assert isinstance(token_logprob["token"], str) + assert isinstance(token_logprob["logprob"], (int, float)) + assert isinstance(token_logprob["bytes"], list) + assert isinstance(token_logprob["top_logprobs"], list) + + # Validate top_logprobs structure + for top_logprob in token_logprob["top_logprobs"]: + assert "token" in top_logprob + assert "logprob" in top_logprob + assert "bytes" in top_logprob + + def test_chat_completions_logprobs_false( + self, client, model: str, messages: List[dict] + ): + """Test that logprobs=False returns no logprobs.""" + response = client.post( + "/v1/chat/completions", + json={ + "model": model, + "messages": messages, + "logprobs": False, + "max_tokens": 10, + }, + ) + + assert response.status_code == 200 + response_json = response.json() + + # logprobs should be None when logprobs=False + choice = response_json["choices"][0] + assert choice.get("logprobs") is None + + @pytest.mark.parametrize("top_logprobs_value", [0, 5]) + def test_chat_completions_top_logprobs_without_logprobs( + self, client, model: str, messages: List[dict], top_logprobs_value: int + ): + """Test that top_logprobs without logprobs raises validation error.""" + response = client.post( + "/v1/chat/completions", + json={ + "model": model, + "messages": messages, + "top_logprobs": top_logprobs_value, + "max_tokens": 10, + }, + ) + + # Should raise validation error for any value when logprobs is not True + assert response.status_code == 400 + assert ( + "`top_logprobs` can only be used when `logprobs` is True" + in response.json()["detail"] + ) + + def test_chat_completions_top_logprobs_validation( + self, client, model: str, messages: List[dict] + ): + """Test that top_logprobs > 20 is rejected by schema validation.""" + response = client.post( + "/v1/chat/completions", + json={ + "model": model, + "messages": messages, + "logprobs": True, + "top_logprobs": 25, # Exceeds maximum of 20 + "max_tokens": 5, + }, + ) + + # Should raise schema validation error + assert response.status_code == 422 + assert "Input should be less than or equal to 20" in str( + response.json()["detail"] + ) + # For tests that won't use the same pytest fixture for server startup across # the whole class test suite. diff --git a/python/openai/tests/test_completions.py b/python/openai/tests/test_completions.py index 3c88a68acd..ea1cf62166 100644 --- a/python/openai/tests/test_completions.py +++ b/python/openai/tests/test_completions.py @@ -81,10 +81,10 @@ def test_completions_sampling_parameters( print("Response:", response.json()) # FIXME: Add support and remove this check - unsupported_parameters = ["logprobs", "logit_bias"] + unsupported_parameters = ["logit_bias"] if sampling_parameter in unsupported_parameters: assert response.status_code == 400 - assert response.json()["detail"] == "logit bias and log probs not supported" + assert response.json()["detail"] == "logit bias is not supported" return assert response.status_code == 200 @@ -397,3 +397,98 @@ def test_usage_response(self, client, model: str, prompt: str): assert ( usage["total_tokens"] == usage["prompt_tokens"] + usage["completion_tokens"] ) + + def test_completions_logprobs(self, client, backend: str, model: str, prompt: str): + """Test logprobs parameter for completions.""" + response = client.post( + "/v1/completions", + json={ + "model": model, + "prompt": prompt, + "logprobs": 3, + "max_tokens": 10, + }, + ) + + # Non-vLLM backends should raise an error + if backend != "vllm": + assert response.status_code == 400 + assert ( + "logprobs are currently available only for the vLLM backend" + in response.json()["detail"] + ) + return + + assert response.status_code == 200 + response_json = response.json() + + # Check that logprobs are present in the response + choice = response_json["choices"][0] + assert "logprobs" in choice + logprobs = choice["logprobs"] + + assert logprobs is not None + assert "text_offset" in logprobs + assert "token_logprobs" in logprobs + assert "tokens" in logprobs + assert "top_logprobs" in logprobs + + assert isinstance(logprobs["text_offset"], list) + assert isinstance(logprobs["token_logprobs"], list) + assert isinstance(logprobs["tokens"], list) + assert isinstance(logprobs["top_logprobs"], list) + + # All lists should have the same length + num_tokens = len(logprobs["tokens"]) + assert len(logprobs["text_offset"]) == num_tokens + assert len(logprobs["token_logprobs"]) == num_tokens + assert len(logprobs["top_logprobs"]) == num_tokens + + # Validate each token + for i in range(num_tokens): + assert isinstance(logprobs["tokens"][i], str) + assert isinstance(logprobs["token_logprobs"][i], (int, float)) + assert isinstance(logprobs["text_offset"][i], int) + assert isinstance(logprobs["top_logprobs"][i], dict) + + # Validate top_logprobs dict contains token -> logprob mappings + for token, logprob in logprobs["top_logprobs"][i].items(): + assert isinstance(token, str) + assert isinstance(logprob, (int, float)) + + def test_completions_logprobs_zero(self, client, model: str, prompt: str): + """Test that logprobs=0 returns no logprobs.""" + response = client.post( + "/v1/completions", + json={ + "model": model, + "prompt": prompt, + "logprobs": 0, + "max_tokens": 10, + }, + ) + + assert response.status_code == 200 + response_json = response.json() + + # logprobs should be None when logprobs=0 + choice = response_json["choices"][0] + assert choice.get("logprobs") is None + + def test_completions_logprobs_validation(self, client, model: str, prompt: str): + """Test that logprobs > 5 is rejected by schema validation.""" + response = client.post( + "/v1/completions", + json={ + "model": model, + "prompt": prompt, + "logprobs": 7, # Exceeds maximum of 5 + "max_tokens": 5, + }, + ) + + # Should raise schema validation error + assert response.status_code == 422 + assert "Input should be less than or equal to 5" in str( + response.json()["detail"] + ) diff --git a/python/openai/tests/test_models/mock_llm/config.pbtxt b/python/openai/tests/test_models/mock_llm/config.pbtxt index 5f665ff543..cac10b5de0 100644 --- a/python/openai/tests/test_models/mock_llm/config.pbtxt +++ b/python/openai/tests/test_models/mock_llm/config.pbtxt @@ -1,4 +1,4 @@ -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -41,6 +41,12 @@ input [ name: "stream" data_type: TYPE_BOOL dims: [ 1, 1 ] + }, + { + name: "return_logprobs" + data_type: TYPE_BOOL + dims: [ 1, 1 ] + optional: true } ] diff --git a/python/openai/tests/test_openai_client.py b/python/openai/tests/test_openai_client.py index 0c02f69db3..e5ce4ae55e 100644 --- a/python/openai/tests/test_openai_client.py +++ b/python/openai/tests/test_openai_client.py @@ -26,6 +26,7 @@ from typing import List +import numpy as np import openai import pytest @@ -482,3 +483,234 @@ async def test_stream_options_without_streaming( stream_options={"include_usage": True}, ) assert "`stream_options` can only be used when `stream` is True" in str(e.value) + + @pytest.mark.asyncio + async def test_chat_completion_logprobs( + self, client: openai.AsyncOpenAI, backend: str, model: str, messages: List[dict] + ): + """Test logprobs for chat completions and compare streaming vs non-streaming.""" + # Non-vLLM backends should raise an error + if backend != "vllm": + with pytest.raises(openai.BadRequestError) as exc_info: + await client.chat.completions.create( + model=model, + messages=messages, + logprobs=True, + top_logprobs=2, + max_tokens=10, + ) + assert "logprobs are currently available only for the vLLM backend" in str( + exc_info.value + ) + return + + # Test non-streaming + seed = 0 + temperature = 0.0 + chat_completion = await client.chat.completions.create( + model=model, + messages=messages, + logprobs=True, + top_logprobs=2, + max_tokens=10, + temperature=temperature, + seed=seed, + stream=False, + ) + + assert chat_completion.choices[0].message.content + assert chat_completion.choices[0].logprobs is not None + + logprobs = chat_completion.choices[0].logprobs + assert logprobs.content is not None + assert len(logprobs.content) > 0 + + # Validate each token logprob + for token_logprob in logprobs.content: + assert token_logprob.token + assert isinstance(token_logprob.logprob, float) + assert isinstance(token_logprob.bytes, list) + assert token_logprob.top_logprobs is not None + assert len(token_logprob.top_logprobs) > 0 + + # Test streaming and compare with non-streaming + stream = await client.chat.completions.create( + model=model, + messages=messages, + logprobs=True, + top_logprobs=2, + max_tokens=10, + temperature=temperature, + seed=seed, + stream=True, + ) + + chunks = [] + stream_logprobs = [] + async for chunk in stream: + if chunk.choices[0].delta.content: + chunks.append(chunk.choices[0].delta.content) + if chunk.choices[0].logprobs and chunk.choices[0].logprobs.content: + stream_logprobs.extend(chunk.choices[0].logprobs.content) + + # Assert streaming output matches non-streaming + streamed_output = "".join(chunks) + assert streamed_output == chat_completion.choices[0].message.content + + # Assert both streaming and non-streaming produce logprobs + assert len(stream_logprobs) > 0, "Streaming should produce logprobs" + assert len(stream_logprobs) == len(logprobs.content), "Same number of tokens" + + # Compare tokens and logprob values (using np.allclose for float comparison) + stream_tokens_list = [t.token for t in stream_logprobs] + non_stream_tokens_list = [t.token for t in logprobs.content] + stream_logprobs_values = [t.logprob for t in stream_logprobs] + non_stream_logprobs_values = [t.logprob for t in logprobs.content] + + assert stream_tokens_list == non_stream_tokens_list, "Tokens should match" + assert np.allclose( + stream_logprobs_values, non_stream_logprobs_values, rtol=0, atol=1e-2 + ), "Logprob values should be close" + + @pytest.mark.asyncio + async def test_completion_logprobs( + self, client: openai.AsyncOpenAI, backend: str, model: str, prompt: str + ): + """Test logprobs for completions.""" + # Non-vLLM backends should raise an error + if backend != "vllm": + with pytest.raises(openai.BadRequestError) as exc_info: + await client.completions.create( + model=model, + prompt=prompt, + logprobs=3, + max_tokens=10, + ) + assert "logprobs are currently available only for the vLLM backend" in str( + exc_info.value + ) + return + + # Test non-streaming + seed = 0 + temperature = 0.0 + completion = await client.completions.create( + model=model, + prompt=prompt, + logprobs=3, + max_tokens=10, + temperature=temperature, + seed=seed, + stream=False, + ) + + assert completion.choices[0].text + assert completion.choices[0].logprobs is not None + + logprobs = completion.choices[0].logprobs + assert logprobs.tokens is not None + assert logprobs.token_logprobs is not None + assert logprobs.text_offset is not None + assert logprobs.top_logprobs is not None + + num_tokens = len(logprobs.tokens) + assert len(logprobs.token_logprobs) == num_tokens + assert len(logprobs.text_offset) == num_tokens + assert len(logprobs.top_logprobs) == num_tokens + + # Test streaming and compare with non-streaming + stream = await client.completions.create( + model=model, + prompt=prompt, + logprobs=3, + max_tokens=10, + temperature=temperature, + seed=seed, + stream=True, + ) + + chunks = [] + stream_tokens = [] + stream_token_logprobs = [] + stream_text_offsets = [] + stream_top_logprobs = [] + + async for chunk in stream: + if chunk.choices[0].text: + chunks.append(chunk.choices[0].text) + if chunk.choices[0].logprobs: + lp = chunk.choices[0].logprobs + if lp.tokens: + stream_tokens.extend(lp.tokens) + if lp.token_logprobs: + stream_token_logprobs.extend(lp.token_logprobs) + if lp.text_offset: + stream_text_offsets.extend(lp.text_offset) + if lp.top_logprobs: + stream_top_logprobs.extend(lp.top_logprobs) + + # Assert streaming output matches non-streaming + streamed_output = "".join(chunks) + assert streamed_output == completion.choices[0].text + + # Compare values (using np.allclose for float comparison) + assert stream_tokens == logprobs.tokens, "Tokens should match" + assert stream_text_offsets == logprobs.text_offset, "Text offsets should match" + assert stream_top_logprobs == logprobs.top_logprobs, "Top logprobs should match" + assert np.allclose( + stream_token_logprobs, logprobs.token_logprobs, rtol=0, atol=1e-2 + ), "Token logprob values should be close" + + @pytest.mark.parametrize("top_logprobs_value", [0, 5]) + @pytest.mark.asyncio + async def test_top_logprobs_requires_logprobs( + self, + client: openai.AsyncOpenAI, + model: str, + messages: List[dict], + top_logprobs_value: int, + ): + """ + Test that top_logprobs without logprobs raises an error + """ + with pytest.raises(openai.BadRequestError) as exc_info: + await client.chat.completions.create( + model=model, + messages=messages, + top_logprobs=top_logprobs_value, # Without logprobs=True + max_tokens=5, + ) + assert "`top_logprobs` can only be used when `logprobs` is True" in str( + exc_info.value + ) + + @pytest.mark.asyncio + async def test_chat_top_logprobs_exceeds_max( + self, client: openai.AsyncOpenAI, model: str, messages: List[dict] + ): + """Test that top_logprobs > 20 raises schema validation error.""" + with pytest.raises(openai.UnprocessableEntityError) as exc_info: + await client.chat.completions.create( + model=model, + messages=messages, + logprobs=True, + top_logprobs=25, # Exceeds maximum of 20 + max_tokens=5, + ) + # Pydantic validation error + assert "less than or equal to 20" in str(exc_info.value).lower() + + @pytest.mark.asyncio + async def test_completion_logprobs_exceeds_max( + self, client: openai.AsyncOpenAI, model: str, prompt: str + ): + """Test that logprobs > 5 raises schema validation error.""" + with pytest.raises(openai.UnprocessableEntityError) as exc_info: + await client.completions.create( + model=model, + prompt=prompt, + logprobs=7, # Exceeds maximum of 5 + max_tokens=5, + ) + # Pydantic validation error + assert "less than or equal to 5" in str(exc_info.value).lower() From 31451dcffe2398b76029684fcaf2f9fa748fa5cb Mon Sep 17 00:00:00 2001 From: J Wyman Date: Thu, 4 Dec 2025 11:43:04 -0500 Subject: [PATCH 113/230] docs: Reformat Model Repository Files (#8493) --- docs/user_guide/model_configuration.md | 599 +++++++++---------------- docs/user_guide/model_management.md | 304 +++++-------- docs/user_guide/model_repository.md | 279 ++++-------- 3 files changed, 404 insertions(+), 778 deletions(-) diff --git a/docs/user_guide/model_configuration.md b/docs/user_guide/model_configuration.md index 497c9c6ccb..55435a296c 100644 --- a/docs/user_guide/model_configuration.md +++ b/docs/user_guide/model_configuration.md @@ -28,37 +28,20 @@ # Model Configuration -**Is this your first time writing a config file?** Check out -[this guide](https://github.com/triton-inference-server/tutorials/tree/main/Conceptual_Guide/Part_1-model_deployment#model-configuration) - or this -[example](https://github.com/triton-inference-server/tutorials/tree/main/HuggingFace#examples)! - -Each model in a [model repository](model_repository.md) must include a -model configuration that provides required and optional information -about the model. Typically, this configuration is provided in a -config.pbtxt file specified as [ModelConfig -protobuf](https://github.com/triton-inference-server/common/blob/main/protobuf/model_config.proto). -In some cases, discussed in [Auto-Generated Model -Configuration](#auto-generated-model-configuration), the model -configuration can be generated automatically by Triton and so does not -need to be provided explicitly. - -This section describes the most important model configuration -properties but the documentation in the [ModelConfig -protobuf](https://github.com/triton-inference-server/common/blob/main/protobuf/model_config.proto) -should also be consulted. +**Is this your first time writing a config file?** Check out [this guide](https://github.com/triton-inference-server/tutorials/tree/main/Conceptual_Guide/Part_1-model_deployment#model-configuration) or this [example](https://github.com/triton-inference-server/tutorials/tree/main/HuggingFace#examples)! + +Each model in a [model repository](model_repository.md) must include a model configuration that provides required and optional information about the model. +Typically, this configuration is provided in a config.pbtxt file specified as [ModelConfig protobuf](https://github.com/triton-inference-server/common/blob/main/protobuf/model_config.proto). +In some cases, discussed in [Auto-Generated Model Configuration](#auto-generated-model-configuration), the model configuration can be generated automatically by Triton and so does not need to be provided explicitly. + +This section describes the most important model configuration properties but the documentation in the [ModelConfig protobuf](https://github.com/triton-inference-server/common/blob/main/protobuf/model_config.proto) should also be consulted. ## Minimal Model Configuration -A minimal model configuration must specify the [*platform* and/or -*backend* -properties](https://github.com/triton-inference-server/backend/blob/main/README.md#backends), -the *max_batch_size* property, and the input and output tensors of the -model. +A minimal model configuration must specify the [*platform* and/or *backend* properties](https://github.com/triton-inference-server/backend/blob/main/README.md#backends), the *max_batch_size* property, and the input and output tensors of the model. -As an example consider a TensorRT model that has two inputs, *input0* -and *input1*, and one output, *output0*, all of which are 16 entry -float32 tensors. The minimal configuration is: +As an example consider a TensorRT model that has two inputs, *input0* and *input1*, and one output, *output0*, all of which are 16 entry float32 tensors. +The minimal configuration is: ``` platform: "tensorrt_plan" @@ -86,118 +69,79 @@ float32 tensors. The minimal configuration is: ### Name, Platform and Backend -The model configuration *name* property is optional. If the name of -the model is not specified in the configuration it is assumed to be -the same as the model repository directory containing the model. If -*name* is specified it must match the name of the model repository -directory containing the model. The required values for *platform* -and *backend* are described in the [backend -documentation](https://github.com/triton-inference-server/backend/blob/main/README.md#backends). +The model configuration *name* property is optional. +If the name of the model is not specified in the configuration it is assumed to be the same as the model repository directory containing the model. +If *name* is specified it must match the name of the model repository directory containing the model. +The required values for *platform* and *backend* are described in the [backend documentation](https://github.com/triton-inference-server/backend/blob/main/README.md#backends). ### Model Transaction Policy -The *model_transaction_policy* property describes the nature of -transactions expected from the model. +The *model_transaction_policy* property describes the nature of transactions expected from the model. #### Decoupled -This boolean setting indicates whether responses generated by -the model are [decoupled](./decoupled_models.md) -with the requests issued to it. Using decoupled means the number of -responses generated by the model may differ from number of requests -issued, and the responses may be out of order relative to the order -of requests. The default is false, which means the model will -generate exactly one response for each request. +This boolean setting indicates whether responses generated by the model are [decoupled](./decoupled_models.md) with the requests issued to it. +Using decoupled means the number of responses generated by the model may differ from number of requests issued, and the responses may be out of order relative to the order of requests. +The default is false, which means the model will generate exactly one response for each request. ### Maximum Batch Size -The *max_batch_size* property indicates the maximum batch size that -the model supports for the [types of -batching](architecture.md#models-and-schedulers) that can be exploited -by Triton. If the model's batch dimension is the first dimension, and -all inputs and outputs to the model have this batch dimension, then -Triton can use its [dynamic batcher](#dynamic-batcher) or [sequence -batcher](#sequence-batcher) to automatically use batching with the -model. In this case *max_batch_size* should be set to a value -greater-or-equal-to 1 that indicates the maximum batch size that -Triton should use with the model. +The *max_batch_size* property indicates the maximum batch size that the model supports for the [types of batching](architecture.md#models-and-schedulers) that can be exploited by Triton. +If the model's batch dimension is the first dimension, and all inputs and outputs to the model have this batch dimension, then Triton can use its [dynamic batcher](#dynamic-batcher) or [sequence batcher](#sequence-batcher) to automatically use batching with the model. +In this case *max_batch_size* should be set to a value greater-or-equal-to 1 that indicates the maximum batch size that Triton should use with the model. -For models that do not support batching, or do not support batching in -the specific ways described above, *max_batch_size* must be set to -zero. +For models that do not support batching, or do not support batching in the specific ways described above, *max_batch_size* must be set to zero. ### Inputs and Outputs Each model input and output must specify a name, datatype, and shape. -The name specified for an input or output tensor must match the name -expected by the model. +The name specified for an input or output tensor must match the name expected by the model. #### Special Conventions for PyTorch Backend **Naming Convention:** -Due to the absence of sufficient metadata for inputs/outputs in TorchScript -model files, the "name" attribute of inputs/outputs in the configuration must -follow specific naming conventions. These are detailed below. +Due to the absence of sufficient metadata for inputs/outputs in TorchScript model files, the "name" attribute of inputs/outputs in the configuration must follow specific naming conventions. +These are detailed below. -1. [Only for Inputs] When the input is not a Dictionary of Tensors, the input -names in the configuration file should mirror the names of the input arguments to -the forward function in the model's definition. +1. [Only for Inputs] When the input is not a Dictionary of Tensors, the input names in the configuration file should mirror the names of the input arguments to the forward function in the model's definition. -For example, if the forward function for the Torchscript model was defined as -`forward(self, input0, input1)`, the first and second inputs should be named -"input0" and "input1" respectively. + For example, if the forward function for the Torchscript model was defined as `forward(self, input0, input1)`, the first and second inputs should be named "input0" and "input1" respectively. -2. `__`: Where \ can be any string and \ is an -integer index that refers to the position of the corresponding input/output. +2. `__`: Where \ can be any string and \ is an integer index that refers to the position of the corresponding input/output. -This means that if there are two inputs and two outputs, the first and second -inputs can be named "INPUT__0" and "INPUT__1" and the first and second outputs -can be named "OUTPUT__0" and "OUTPUT__1" respectively. + This means that if there are two inputs and two outputs, the first and second inputs can be named "INPUT__0" and "INPUT__1" and the first and second outputs can be named "OUTPUT__0" and "OUTPUT__1" respectively. -3. If all inputs (or outputs) do not follow the same naming convention, then we -enforce strict ordering from the model configuration i.e. we assume the order of -inputs (or outputs) in the configuration is the true ordering of these inputs. +3. If all inputs (or outputs) do not follow the same naming convention, then we enforce strict ordering from the model configuration i.e. we assume the order of inputs (or outputs) in the configuration is the true ordering of these inputs. ***Dictionary of Tensors as Input:*** -The PyTorch backend supports passing of inputs to the model in the form of a -Dictionary of Tensors. This is only supported when there is a *single* input to -the model of type Dictionary that contains a mapping of string to tensor. As an -example, if there is a model that expects the input of the form: +The PyTorch backend supports passing of inputs to the model in the form of a Dictionary of Tensors. +This is only supported when there is a *single* input to the model of type Dictionary that contains a mapping of string to tensor. +As an example, if there is a model that expects the input of the form: ``` {'A': tensor1, 'B': tensor2} ``` -The input names in the configuration in this case must not follow the above -naming conventions `__`. Instead, the names of the inputs in this -case must map to the string value 'key' for that specific tensor. For this case, -the inputs would be "A" and "B", where input "A" refers to value corresponding to -tensor1 and "B" refers to the value corresponding to tensor2. +The input names in the configuration in this case must not follow the above naming conventions `__`. +Instead, the names of the inputs in this case must map to the string value 'key' for that specific tensor. +For this case, the inputs would be "A" and "B", where input "A" refers to value corresponding to tensor1 and "B" refers to the value corresponding to tensor2. -
+
-The datatypes allowed for input and output tensors varies based on the -type of the model. Section [Datatypes](#datatypes) describes the -allowed datatypes and how they map to the datatypes of each model -type. +The datatypes allowed for input and output tensors varies based on the type of the model. +Section [Datatypes](#datatypes) describes the allowed datatypes and how they map to the datatypes of each model type. -An input shape indicates the shape of an input tensor expected by the -model and by Triton in inference requests. An output shape indicates -the shape of an output tensor produced by the model and returned by -Triton in response to an inference request. Both input and output -shape must have rank greater-or-equal-to 1, that is, the empty shape -**[ ]** is not allowed. +An input shape indicates the shape of an input tensor expected by the model and by Triton in inference requests. +An output shape indicates the shape of an output tensor produced by the model and returned by Triton in response to an inference request. +Both input and output shape must have rank greater-or-equal-to 1, that is, the empty shape **[ ]** is not allowed. -Input and output shapes are specified by a combination of -*max_batch_size* and the dimensions specified by the input or output -*dims* property. For models with *max_batch_size* greater-than 0, the -full shape is formed as [ -1 ] + *dims*. For models with -*max_batch_size* equal to 0, the full shape is formed as *dims*. For -example, for the following configuration the shape of "input0" is [ --1, 16 ] and the shape of "output0" is [ -1, 4 ]. +Input and output shapes are specified by a combination of *max_batch_size* and the dimensions specified by the input or output *dims* property. +For models with *max_batch_size* greater-than 0, the full shape is formed as [ -1 ] + *dims*. +For models with *max_batch_size* equal to 0, the full shape is formed as *dims*. +For example, for the following configuration the shape of "input0" is [-1, 16 ] and the shape of "output0" is [ -1, 4 ]. ``` platform: "tensorrt_plan" @@ -218,9 +162,7 @@ example, for the following configuration the shape of "input0" is [ ] ``` -For a configuration that is identical except that *max_batch_size* -equal to 0, the shape of "input0" is [ 16 ] and the shape of "output0" -is [ 4 ]. +For a configuration that is identical except that *max_batch_size* equal to 0, the shape of "input0" is [ 16 ] and the shape of "output0" is [ 4 ]. ``` platform: "tensorrt_plan" @@ -241,103 +183,62 @@ is [ 4 ]. ] ``` -For models that support input and output tensors with variable-size -dimensions, those dimensions can be listed as -1 in the input and -output configuration. For example, if a model requires a 2-dimensional -input tensor where the first dimension must be size 4 but the second -dimension can be any size, the model configuration for that input -would include *dims: [ 4, -1 ]*. Triton would then accept inference -requests where that input tensor's second dimension was any value -greater-or-equal-to 0. The model configuration can be more restrictive -than what is allowed by the underlying model. For example, even though -the framework model itself allows the second dimension to be any size, -the model configuration could be specified as *dims: [ 4, 4 ]*. In -this case, Triton would only accept inference requests where the input -tensor's shape was exactly *[ 4, 4 ]*. - -The [*reshape* property](#reshape) must be used if there is a mismatch -between the input shape that Triton receives in an inference request -and the input shape expected by the model. Similarly, the *reshape* -property must be used if there is a mismatch between the output shape -produced by the model and the shape that Triton returns in a response -to an inference request. - -Model inputs can specify `allow_ragged_batch` to indicate that the -input is a [ragged input](ragged_batching.md#ragged-batching). The field is -used with [dynamic batcher](#dynamic-batcher) to allow batching without -enforcing the input to have the same shape in all requests. +For models that support input and output tensors with variable-size dimensions, those dimensions can be listed as -1 in the input and output configuration. +For example, if a model requires a 2-dimensional input tensor where the first dimension must be size 4 but the second dimension can be any size, the model configuration for that input would include *dims: [ 4, -1 ]*. +Triton would then accept inference requests where that input tensor's second dimension was any value greater-or-equal-to 0. +The model configuration can be more restrictive than what is allowed by the underlying model. +For example, even though the framework model itself allows the second dimension to be any size, the model configuration could be specified as *dims: [ 4, 4 ]*. +In this case, Triton would only accept inference requests where the input tensor's shape was exactly *[ 4, 4 ]*. + +The [*reshape* property](#reshape) must be used if there is a mismatch between the input shape that Triton receives in an inference request and the input shape expected by the model. +Similarly, the *reshape* property must be used if there is a mismatch between the output shape produced by the model and the shape that Triton returns in a response to an inference request. + +Model inputs can specify `allow_ragged_batch` to indicate that the input is a [ragged input](ragged_batching.md#ragged-batching). +The field is used with [dynamic batcher](#dynamic-batcher) to allow batching without enforcing the input to have the same shape in all requests. ## Auto-Generated Model Configuration -The model configuration file containing the required -settings must be available with each model to be deployed -on Triton. In some cases the required portions of the model -configuration can be generated automatically by Triton. The -required portion of the model configuration are the settings -shown in the [Minimal Model Configuration](#minimal-model-configuration). -By default, Triton will try to complete these sections. However, -by starting Triton with `--disable-auto-complete-config` option, -Triton can be configured to not auto-complete model configuration -on the backend side. However, even with this option Triton will -fill in missing [`instance_group`](#instance-groups) settings with -default values. - -Triton can derive all the required settings automatically for -most of the TensorRT saved-model, ONNX models, and OpenVINO models. -For Python models, [`auto_complete_config`](https://github.com/triton-inference-server/python_backend/#auto_complete_config) -function can be implemented in Python backend to provide -[`max_batch_size`](#maximum-batch-size), [`input`](#inputs-and-outputs) -and [`output`](#inputs-and-outputs) properties using `set_max_batch_size`, -`add_input`, and `add_output` functions. These properties will allow Triton -to load the Python model with [Minimal Model Configuration](#minimal-model-configuration) -in absence of a configuration file. +The model configuration file containing the required settings must be available with each model to be deployed on Triton. +In some cases the required portions of the model configuration can be generated automatically by Triton. +The required portion of the model configuration are the settings shown in the [Minimal Model Configuration](#minimal-model-configuration). +By default, Triton will try to complete these sections. +However, by starting Triton with `--disable-auto-complete-config` option, Triton can be configured to not auto-complete model configuration on the backend side. +However, even with this option Triton will fill in missing [`instance_group`](#instance-groups) settings with default values. + +Triton can derive all the required settings automatically for most of the TensorRT saved-model, ONNX models, and OpenVINO models. +For Python models, [`auto_complete_config`](https://github.com/triton-inference-server/python_backend/#auto_complete_config) function can be implemented in Python backend to provide [`max_batch_size`](#maximum-batch-size), [`input`](#inputs-and-outputs) and [`output`](#inputs-and-outputs) properties using `set_max_batch_size`, `add_input`, and `add_output` functions. +These properties will allow Triton to load the Python model with [Minimal Model Configuration](#minimal-model-configuration) in absence of a configuration file. All other model types *must* provide a model configuration file. -When developing a custom backend, you can populate required settings -in the configuration and call `TRITONBACKEND_ModelSetConfig` API to -update completed configuration with Triton core. You can take a -look at [Onnxruntime](https://github.com/triton-inference-server/onnxruntime_backend) -backends as examples of how to achieve this. Currently, only -[inputs, outputs](#inputs-and-outputs), [max_batch_size](#maximum-batch-size) -and [dynamic batching](#dynamic-batcher) settings can be populated by -backend. For custom backends, your config.pbtxt file must -include a `backend` field or your model name must be in the -form `.`. - -You can also see the model configuration generated for a model by -Triton using the [model configuration endpoint](../protocol/extension_model_configuration.md). The -easiest way to do this is to use a utility like *curl*: +When developing a custom backend, you can populate required settings in the configuration and call `TRITONBACKEND_ModelSetConfig` API to update completed configuration with Triton core. +You can take a look at [Onnxruntime](https://github.com/triton-inference-server/onnxruntime_backend) backends as examples of how to achieve this. +Currently, only [inputs, outputs](#inputs-and-outputs), [max_batch_size](#maximum-batch-size) and [dynamic batching](#dynamic-batcher) settings can be populated by backend. +For custom backends, your config.pbtxt file must include a `backend` field or your model name must be in the form `.`. + +You can also see the model configuration generated for a model by Triton using the [model configuration endpoint](../protocol/extension_model_configuration.md). +The easiest way to do this is to use a utility like *curl*: ```bash $ curl localhost:8000/v2/models//config ``` -This will return a JSON representation of the generated model -configuration. From this you can take the max_batch_size, inputs, and -outputs sections of the JSON and convert it to a config.pbtxt file. -Triton only generates the [minimal portion of the model -configuration](#minimal-model-configuration). You must still provide -the optional portions of the model configuration by editing the -config.pbtxt file. +This will return a JSON representation of the generated model configuration. +From this you can take the max_batch_size, inputs, and outputs sections of the JSON and convert it to a config.pbtxt file. +Triton only generates the [minimal portion of the model configuration](#minimal-model-configuration). +You must still provide the optional portions of the model configuration by editing the config.pbtxt file. ## Custom Model Configuration -Sometimes when multiple devices running Triton instances that share one -model repository, it is necessary to have models configured differently -on each platform in order to achieve the best performance. Triton allows -users to pick the custom model configuration name by setting `--model-config-name` option. +Sometimes when multiple devices running Triton instances that share one model repository, it is necessary to have models configured differently on each platform in order to achieve the best performance. +Triton allows users to pick the custom model configuration name by setting `--model-config-name` option. -For example, when running `./tritonserver --model-repository= --model-config-name=h100`, -the server will search the custom configuration file `h100.pbtxt` under -`/path/to/model/repository//configs` directory for each model -that is loaded. If `h100.pbtxt` exists, it will be used as the configuration -for this model. Otherwise, the default configuration `/path/to/model/repository//config.pbtxt` -or [auto-generated model configuration](#auto-generated-model-configuration) -will be selected based on the settings. +For example, when running `./tritonserver --model-repository= --model-config-name=h100`, the server will search the custom configuration file `h100.pbtxt` under `/path/to/model/repository//configs` directory for each model +that is loaded. +If `h100.pbtxt` exists, it will be used as the configuration for this model. +Otherwise, the default configuration `/path/to/model/repository//config.pbtxt` or [auto-generated model configuration](#auto-generated-model-configuration) will be selected based on the settings. -Custom model configuration also works with `Explicit` and `Poll` model -control modes. Users may delete or add new custom configurations and the -server will pick the configuration file for each loaded model dynamically. +Custom model configuration also works with `Explicit` and `Poll` model control modes. +Users may delete or add new custom configurations and the server will pick the configuration file for each loaded model dynamically. Note: custom model configuration name should not contain any space character. @@ -400,40 +301,30 @@ Example 3: --model-config-name not set ### Default Max Batch Size and Dynamic Batcher -When a model is using the auto-complete feature, a default maximum -batch size may be set by using the `--backend-config=default-max-batch-size=` -command line argument. This allows all models which are capable of -batching and which make use of [Auto Generated Model Configuration](#auto-generated-model-configuration) -to have a default maximum batch size. This value is set to 4 by -default. Backend developers may make use of this default-max-batch-size -by obtaining it from the TRITONBACKEND_BackendConfig api. Currently, the -following backends which utilize these default batch values and turn on -dynamic batching in their generated model configurations are: +When a model is using the auto-complete feature, a default maximum batch size may be set by using the `--backend-config=default-max-batch-size=` command line argument. +This allows all models which are capable of batching and which make use of [Auto Generated Model Configuration](#auto-generated-model-configuration) to have a default maximum batch size. +This value is set to 4 by default. +Backend developers may make use of this default-max-batch-size by obtaining it from the TRITONBACKEND_BackendConfig api. +Currently, the following backends which utilize these default batch values and turn on dynamic batching in their generated model configurations are: 1. [Onnxruntime backend](https://github.com/triton-inference-server/onnxruntime_backend) + 2. [TensorRT backend](https://github.com/triton-inference-server/tensorrt_backend) - 1. TensorRT models store the maximum batch size explicitly and do not make use - of the default-max-batch-size parameter. However, if max_batch_size > 1 - and no [scheduler](model_configuration.md#scheduling-and-batching) - is provided, the dynamic batch scheduler will be enabled. -If a value greater than 1 for the maximum batch size is set for the -model, the [dynamic_batching](#dynamic-batcher) config will be set -if no scheduler is provided in the configuration file. + 1. TensorRT models store the maximum batch size explicitly and do not make use of the default-max-batch-size parameter. + However, if max_batch_size > 1 and no [scheduler](model_configuration.md#scheduling-and-batching) is provided, the dynamic batch scheduler will be enabled. + +If a value greater than 1 for the maximum batch size is set for the model, the [dynamic_batching](#dynamic-batcher) config will be set if no scheduler is provided in the configuration file. ## Datatypes -The following table shows the tensor datatypes supported by -Triton. The first column shows the name of the datatype as it appears -in the model configuration file. The next four columns show the -corresponding datatype for supported model frameworks. If a model -framework does not have an entry for a given datatype, then Triton -does not support that datatype for that model. The sixth column, -labeled "API", shows the corresponding datatype for the TRITONSERVER C -API, TRITONBACKEND C API, HTTP/REST protocol and GRPC protocol. The -last column shows the corresponding datatype for the Python numpy -library. +The following table shows the tensor datatypes supported by Triton. +The first column shows the name of the datatype as it appears in the model configuration file. +The next four columns show the corresponding datatype for supported model frameworks. +If a model framework does not have an entry for a given datatype, then Triton does not support that datatype for that model. +The sixth column, labeled "API", shows the corresponding datatype for the TRITONSERVER C API, TRITONBACKEND C API, HTTP/REST protocol and GRPC protocol. +The last column shows the corresponding datatype for the Python numpy library. |Model Config |TensorRT |ONNX Runtime |PyTorch |API |NumPy | |--------------|--------------|--------------|---------|---------|--------------| @@ -450,37 +341,26 @@ library. |TYPE_FP32 | kFLOAT |FLOAT |kFloat |FP32 |float32 | |TYPE_FP64 | |DOUBLE |kDouble |FP64 |float64 | |TYPE_STRING | |STRING | |BYTES |dtype(object) | -|TYPE_BF16 | kBF16 | | | |BF16 | | +|TYPE_BF16 | kBF16 | | |BF16 | | -For TensorRT each value is in the nvinfer1::DataType namespace. For -example, nvinfer1::DataType::kFLOAT is the 32-bit floating-point -datatype. +For TensorRT each value is in the nvinfer1::DataType namespace. +For example, nvinfer1::DataType::kFLOAT is the 32-bit floating-point datatype. For ONNX Runtime each value is prepended with ONNX_TENSOR_ELEMENT_DATA_TYPE_. -For example, ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT is the 32-bit floating-point -datatype. +For example, ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT is the 32-bit floating-point datatype. -For PyTorch each value is in the torch namespace. For example, torch::kFloat -is the 32-bit floating-point datatype. +For PyTorch each value is in the torch namespace. For example, torch::kFloat is the 32-bit floating-point datatype. -For Numpy each value is in the numpy module. For example, numpy.float32 -is the 32-bit floating-point datatype. +For Numpy each value is in the numpy module. For example, numpy.float32 is the 32-bit floating-point datatype. ## Reshape -The *ModelTensorReshape* property on a model configuration input or -output is used to indicate that the input or output shape accepted by -the inference API differs from the input or output shape expected or -produced by the underlying framework model or custom backend. +The *ModelTensorReshape* property on a model configuration input or output is used to indicate that the input or output shape accepted by the inference API differs from the input or output shape expected or produced by the underlying framework model or custom backend. -For an input, *reshape* can be used to reshape the input tensor to a -different shape expected by the framework or backend. A common -use-case is where a model that supports batching expects a batched -input to have shape *[ batch-size ]*, which means that the batch -dimension fully describes the shape. For the inference API the -equivalent shape *[ batch-size, 1 ]* must be specified since each -input must specify a non-empty *dims*. For this case the input should -be specified as: +For an input, *reshape* can be used to reshape the input tensor to a different shape expected by the framework or backend. +A common use-case is where a model that supports batching expects a batched input to have shape *[ batch-size ]*, which means that the batch dimension fully describes the shape. +For the inference API the equivalent shape *[ batch-size, 1 ]* must be specified since each input must specify a non-empty *dims*. +For this case the input should be specified as: ``` input [ @@ -489,16 +369,13 @@ be specified as: dims: [ 1 ] reshape: { shape: [ ] } } + ] ``` -For an output, *reshape* can be used to reshape the output tensor -produced by the framework or backend to a different shape that is -returned by the inference API. A common use-case is where a model that -supports batching expects a batched output to have shape *[ batch-size -]*, which means that the batch dimension fully describes the -shape. For the inference API the equivalent shape *[ batch-size, 1 ]* -must be specified since each output must specify a non-empty -*dims*. For this case the output should be specified as: +For an output, *reshape* can be used to reshape the output tensor produced by the framework or backend to a different shape that is returned by the inference API. +A common use-case is where a model that supports batching expects a batched output to have shape *[ batch-size ]*, which means that the batch dimension fully describes the shape. +For the inference API the equivalent shape *[ batch-size, 1 ]* must be specified since each output must specify a non-empty *dims*. +For this case the output should be specified as: ``` output [ @@ -507,14 +384,13 @@ must be specified since each output must specify a non-empty dims: [ 1 ] reshape: { shape: [ ] } } + ] ``` ## Shape Tensors -For models that support shape tensors, the *is_shape_tensor* property -must be set appropriately for inputs and outputs that are acting as -shape tensors. The following shows an example configuration that -specifies shape tensors. +For models that support shape tensors, the *is_shape_tensor* property must be set appropriately for inputs and outputs that are acting as shape tensors. +The following shows an example configuration that specifies shape tensors. ``` name: "myshapetensormodel" @@ -542,11 +418,9 @@ specifies shape tensors. ] ``` -As discussed above, Triton assumes that batching occurs along the -first dimension which is not listed in in the input or output tensor -*dims*. However, for shape tensors, batching occurs at the first shape -value. For the above example, an inference request must provide inputs -with the following shapes. +As discussed above, Triton assumes that batching occurs along the first dimension which is not listed in in the input or output tensor *dims*. +However, for shape tensors, batching occurs at the first shape value. +For the above example, an inference request must provide inputs with the following shapes. ``` "input0": [ x, 1, 3] @@ -554,16 +428,12 @@ with the following shapes. "output0": [ x, 1, 3] ``` -Where *x* is the batch size of the request. Triton requires the shape -tensors to be marked as shape tensors in the model when using -batching. Note that "input1" has shape *[ 3 ]* and not *[ 2 ]*, which -is how it is described in model configuration. As `myshapetensormodel` -model is a batching model, the batch size should be provided as an -additional value. Triton will accumulate all the shape values together -for "input1" in batch dimension before issuing the request to model. +Where *x* is the batch size of the request. +Triton requires the shape tensors to be marked as shape tensors in the model when using batching. Note that "input1" has shape *[ 3 ]* and not *[ 2 ]*, which is how it is described in model configuration. +As `myshapetensormodel` model is a batching model, the batch size should be provided as an additional value. +Triton will accumulate all the shape values together for "input1" in batch dimension before issuing the request to model. -For example, assume the client sends following three requests to Triton -with following inputs: +For example, assume the client sends following three requests to Triton with following inputs: ``` Request1: @@ -579,8 +449,7 @@ input0: [[[10,11,12]]] <== shape of this tensor [1,1,3] input1: [1,4,6] <== shape of this tensor [3] ``` -Assuming these requests get batched together would be delivered to the -model as: +Assuming these requests get batched together would be delivered to the model as: ``` @@ -590,13 +459,13 @@ input1: [4, 4, 6] <== shape of this tensor [3] ``` -Currently, only TensorRT supports shape tensors. Read [Shape Tensor I/O](https://docs.nvidia.com/deeplearning/tensorrt/developer-guide/index.html#shape_tensor_io) -to learn more about shape tensors. +Currently, only TensorRT supports shape tensors. +Read [Shape Tensor I/O](https://docs.nvidia.com/deeplearning/tensorrt/developer-guide/index.html#shape_tensor_io) to learn more about shape tensors. ## Non-Linear I/O Formats -For models that process input or output data in non-linear formats, the _is_non_linear_format_io_ property -must be set. The following example model configuration shows how to specify that INPUT0 and INPUT1 use non-linear I/O data formats. +For models that process input or output data in non-linear formats, the _is_non_linear_format_io_ property must be set. +The following example model configuration shows how to specify that INPUT0 and INPUT1 use non-linear I/O data formats. ``` name: "mytensorrtmodel" @@ -625,37 +494,28 @@ must be set. The following example model configuration shows how to specify that ] ``` -Currently, only TensorRT supports this property. To learn more about I/O formats, refer to the [I/O Formats documentation](https://docs.nvidia.com/deeplearning/tensorrt/developer-guide/index.html#reformat-free-network-tensors). +Currently, only TensorRT supports this property. +To learn more about I/O formats, refer to the [I/O Formats documentation](https://docs.nvidia.com/deeplearning/tensorrt/developer-guide/index.html#reformat-free-network-tensors). ## Version Policy -Each model can have one or more -[versions](model_repository.md#model-versions). The -*ModelVersionPolicy* property of the model configuration is used to -set one of the following policies. +Each model can have one or more [versions](model_repository.md#model-versions). +The *ModelVersionPolicy* property of the model configuration is used to set one of the following policies. -* *All*: All versions of the model that are available in the model - repository are available for inferencing. +* *All*: All versions of the model that are available in the model repository are available for inferencing. ```version_policy: { all: {}}``` -* *Latest*: Only the latest ‘n’ versions of the model in the - repository are available for inferencing. The latest versions of the - model are the numerically greatest version numbers. +* *Latest*: Only the latest ‘n’ versions of the model in the repository are available for inferencing. + The latest versions of the model are the numerically greatest version numbers. ```version_policy: { latest: { num_versions: 2}}``` -* *Specific*: Only the specifically listed versions of the model are - available for inferencing. +* *Specific*: Only the specifically listed versions of the model are available for inferencing. ```version_policy: { specific: { versions: [1,3]}}``` -If no version policy is specified, then *Latest* (with n=1) is used as -the default, indicating that only the most recent version of the model -is made available by Triton. In all cases, the [addition or removal of -version subdirectories](model_management.md) from the model repository -can change which model version is used on subsequent inference -requests. +If no version policy is specified, then *Latest* (with n=1) is used as the default, indicating that only the most recent version of the model is made available by Triton. +In all cases, the [addition or removal of version subdirectories](model_management.md) from the model repository can change which model version is used on subsequent inference requests. -The following configuration specifies that all versions of the model -will be available from the server. +The following configuration specifies that all versions of the model will be available from the server. ``` platform: "tensorrt_plan" @@ -686,18 +546,17 @@ will be available from the server. Triton can provide multiple [instances of a model](architecture.md#concurrent-model-execution) so that multiple -inference requests for that model can be handled simultaneously. The +inference requests for that model can be handled simultaneously. +The model configuration *ModelInstanceGroup* property is used to specify the number of execution instances that should be made available and what compute resource should be used for those instances. ### Multiple Model Instances -By default, a single execution instance of the model is created for -each GPU available in the system. The instance-group setting can be -used to place multiple execution instances of a model on every GPU or -on only certain GPUs. For example, the following configuration will -place two execution instances of the model to be available on each +By default, a single execution instance of the model is created for each GPU available in the system. +The instance-group setting can be used to place multiple execution instances of a model on every GPU or on only certain GPUs. +For example, the following configuration will place two execution instances of the model to be available on each system GPU. ``` @@ -709,8 +568,7 @@ system GPU. ] ``` -And the following configuration will place one execution instance on -GPU 0 and two execution instances on GPUs 1 and 2. +And the following configuration will place one execution instance on GPU 0 and two execution instances on GPUs 1 and 2. ``` instance_group [ @@ -726,14 +584,14 @@ GPU 0 and two execution instances on GPUs 1 and 2. } ] ``` -For a more detailed example of using instance groups, see - [this guide](https://github.com/triton-inference-server/tutorials/tree/main/Conceptual_Guide/Part_2-improving_resource_utilization#concurrent-model-execution). + +For a more detailed example of using instance groups, see [this guide](https://github.com/triton-inference-server/tutorials/tree/main/Conceptual_Guide/Part_2-improving_resource_utilization#concurrent-model-execution). + ### CPU Model Instance -The instance group setting is also used to enable execution of a model -on the CPU. A model can be executed on the CPU even if there is a GPU -available in the system. The following places two execution instances -on the CPU. +The instance group setting is also used to enable execution of a model on the CPU. +A model can be executed on the CPU even if there is a GPU available in the system. +The following places two execution instances on the CPU. ``` instance_group [ @@ -744,17 +602,14 @@ on the CPU. ] ``` -If no `count` is specified for a KIND_CPU instance group, then the default instance -count will be 2 for selected backends (Onnxruntime). All -other backends will default to 1. +If no `count` is specified for a KIND_CPU instance group, then the default instance count will be 2 for selected backends (Onnxruntime). +All other backends will default to 1. ### Host Policy -The instance group setting is associated with a host policy. The following -configuration will associate all instances created by the instance group setting -with host policy "policy_0". By default the host policy will be set according to -the device kind of the instance, for instance, KIND_CPU is "cpu", KIND_MODEL is -"model", and KIND_GPU is "gpu_\". +The instance group setting is associated with a host policy. +The following configuration will associate all instances created by the instance group setting with host policy "policy_0". +By default the host policy will be set according to the device kind of the instance, for instance, KIND_CPU is "cpu", KIND_MODEL is "model", and KIND_GPU is "gpu_\". ``` instance_group [ @@ -768,38 +623,27 @@ the device kind of the instance, for instance, KIND_CPU is "cpu", KIND_MODEL is ### Rate Limiter Configuration -Instance group optionally specifies [rate limiter](rate_limiter.md) -configuration which controls how the rate limiter operates on the -instances in the group. The rate limiter configuration is ignored if -rate limiting is off. If rate limiting is on and if an instance_group -does not provide this configuration, then the execution on the model -instances belonging to this group will not be limited in any way by -the rate limiter. The configuration includes the following -specifications: +Instance group optionally specifies [rate limiter](rate_limiter.md) configuration which controls how the rate limiter operates on the instances in the group. +The rate limiter configuration is ignored if rate limiting is off. +If rate limiting is on and if an instance_group does not provide this configuration, then the execution on the model instances belonging to this group will not be limited in any way by the rate limiter. +The configuration includes the following specifications: #### Resources -The set of [resources](rate_limiter.md#resources) required to execute -a model instance. The "name" field identifies the resource and "count" -field refers to the number of copies of the resource that the model -instance in the group requires to run. The "global" field specifies -whether the resource is per-device or shared globally across the system. -Loaded models can not specify a resource with the same name both as global -and non-global. If no resources are provided then triton assumes the -execution of model instance does not require any resources and will -start executing as soon as model instance is available. +The set of [resources](rate_limiter.md#resources) required to execute a model instance. +The "name" field identifies the resource and "count" field refers to the number of copies of the resource that the model instance in the group requires to run. +The "global" field specifies whether the resource is per-device or shared globally across the system. +Loaded models can not specify a resource with the same name both as global and non-global. +If no resources are provided then triton assumes the execution of model instance does not require any resources and will start executing as soon as model instance is available. #### Priority -Priority serves as a weighting value to be used for prioritizing across -all the instances of all the models. An instance with priority 2 will be -given 1/2 the number of scheduling chances as an instance with priority -1. +Priority serves as a weighting value to be used for prioritizing across all the instances of all the models. +An instance with priority 2 will be given 1/2 the number of scheduling chances as an instance with priority 1. -The following example specifies the instances in the group requires -four "R1" and two "R2" resources for execution. Resource "R2" is a global -resource. Additionally, the rate-limiter priority of the instance_group -is 2. +The following example specifies the instances in the group requires four "R1" and two "R2" resources for execution. +Resource "R2" is a global resource. +Additionally, the rate-limiter priority of the instance_group is 2. ``` instance_group [ @@ -825,34 +669,21 @@ is 2. ] ``` -The above configuration creates 3 model instances, one on each device -(0, 1 and 2). The three instances will not contend for "R1" among -themselves as "R1" is local for their own device, however, they will -contend for "R2" because it is specified as a global resource which -means "R2" is shared across the system. Though these instances don't -contend for "R1" among themselves, but they will contend for "R1" -with other model instances which includes "R1" in their resource -requirements and run on the same device as them. +The above configuration creates 3 model instances, one on each device (0, 1 and 2). +The three instances will not contend for "R1" among themselves as "R1" is local for their own device, however, they will contend for "R2" because it is specified as a global resource which means "R2" is shared across the system. +Though these instances don't contend for "R1" among themselves, but they will contend for "R1" with other model instances which includes "R1" in their resource requirements and run on the same device as them. ### Ensemble Model Instance Groups -[Ensemble models](architecture.md#ensemble-models) -are an abstraction Triton uses to execute a user-defined pipeline of models. -Since there is no physical instance associated with an ensemble model, the -`instance_group` field can not be specified for it. +[Ensemble models](architecture.md#ensemble-models) are an abstraction Triton uses to execute a user-defined pipeline of models. +Since there is no physical instance associated with an ensemble model, the `instance_group` field can not be specified for it. -However, each composing model that makes up an ensemble can specify -`instance_group` in its config file and individually support parallel -execution as described above when the ensemble receives multiple requests. +However, each composing model that makes up an ensemble can specify `instance_group` in its config file and individually support parallel execution as described above when the ensemble receives multiple requests. ## CUDA Compute Capability -Similar to the `default_model_filename` field, you can optionally specify the -`cc_model_filenames` field to map the GPU's -[CUDA Compute Capability](https://developer.nvidia.com/cuda-gpus) -to a corresponding model filename at model load time. This is particularly -useful for TensorRT models, since they are generally tied to a specific -compute capability. +Similar to the `default_model_filename` field, you can optionally specify the `cc_model_filenames` field to map the GPU's [CUDA Compute Capability](https://developer.nvidia.com/cuda-gpus) to a corresponding model filename at model load time. +This is particularly useful for TensorRT models, since they are generally tied to a specific compute capability. ``` cc_model_filenames [ @@ -869,48 +700,28 @@ cc_model_filenames [ ## Optimization Policy -The model configuration *ModelOptimizationPolicy* property is used to -specify optimization and prioritization settings for a model. These -settings control if/how a model is optimized by the backend and how it -is scheduled and executed by Triton. See the [ModelConfig -protobuf](https://github.com/triton-inference-server/common/blob/main/protobuf/model_config.proto) -and [optimization](optimization.md#framework-specific-optimization) -documentation for the currently available settings. +The model configuration *ModelOptimizationPolicy* property is used to specify optimization and prioritization settings for a model. +These settings control if/how a model is optimized by the backend and how it is scheduled and executed by Triton. +See the [ModelConfig protobuf](https://github.com/triton-inference-server/common/blob/main/protobuf/model_config.proto) and [optimization](optimization.md#framework-specific-optimization) documentation for the currently available settings. ## Model Warmup -When a model is loaded by Triton the corresponding -[backend](https://github.com/triton-inference-server/backend/blob/main/README.md) -initializes for that model. For some backends, some or all of this -initialization is deferred until the model receives its first -inference request (or first few inference requests). As a result, the -first (few) inference requests can be significantly slower due to -deferred initialization. - -To avoid these initial, slow inference requests, Triton provides a -configuration option that enables a model to be "warmed up" so that it -is completely initialized before the first inference request is -received. When the *ModelWarmup* property is defined in a model -configuration, Triton will not show the model as being ready for -inference until model warmup has completed. - -The model configuration *ModelWarmup* is used to specify warmup -settings for a model. The settings define a series of inference -requests that Triton will create to warm-up each model instance. A -model instance will be served only if it completes the requests -successfully. Note that the effect of warming up models varies -depending on the framework backend, and it will cause Triton to be -less responsive to model update, so the users should experiment and -choose the configuration that suits their need. See the -[ModelWarmup protobuf](https://github.com/triton-inference-server/common/blob/main/protobuf/model_config.proto) -documentation for the currently available settings, and -[L0_warmup](https://github.com/triton-inference-server/server/blob/main/qa/L0_warmup/test.sh) -for examples on specifying different variants of warmup samples. +When a model is loaded by Triton the corresponding [backend](https://github.com/triton-inference-server/backend/blob/main/README.md) initializes for that model. +For some backends, some or all of this initialization is deferred until the model receives its first inference request (or first few inference requests). +As a result, the first (few) inference requests can be significantly slower due to deferred initialization. + +To avoid these initial, slow inference requests, Triton provides a configuration option that enables a model to be "warmed up" so that it is completely initialized before the first inference request is received. +When the *ModelWarmup* property is defined in a model configuration, Triton will not show the model as being ready for inference until model warmup has completed. + +The model configuration *ModelWarmup* is used to specify warmup settings for a model. +The settings define a series of inference requests that Triton will create to warm-up each model instance. +A model instance will be served only if it completes the requests successfully. +Note that the effect of warming up models varies depending on the framework backend, and it will cause Triton to be less responsive to model update, so the users should experiment and choose the configuration that suits their need. +See the [ModelWarmup protobuf](https://github.com/triton-inference-server/common/blob/main/protobuf/model_config.proto) documentation for the currently available settings, and [L0_warmup](https://github.com/triton-inference-server/server/blob/main/qa/L0_warmup/test.sh) for examples on specifying different variants of warmup samples. ## Response Cache -The model configuration `response_cache` section has an `enable` boolean used to -enable the Response Cache for this model. +The model configuration `response_cache` section has an `enable` boolean used to enable the Response Cache for this model. ``` response_cache { @@ -918,7 +729,5 @@ response_cache { } ``` -In addition to enabling the cache in the model config, a `--cache-config` must -be specified when starting the server to enable caching on the server-side. See -the [Response Cache](response_cache.md) doc for more details on enabling -server-side caching. +In addition to enabling the cache in the model config, a `--cache-config` must be specified when starting the server to enable caching on the server-side. +See the [Response Cache](response_cache.md) doc for more details on enabling server-side caching. diff --git a/docs/user_guide/model_management.md b/docs/user_guide/model_management.md index 4ce698feee..c779ddd48d 100644 --- a/docs/user_guide/model_management.md +++ b/docs/user_guide/model_management.md @@ -1,5 +1,5 @@ -# OpenAI-Compatible Frontend for Triton Inference Server (Beta) - -> [!NOTE] -> The OpenAI-Compatible API is currently in BETA. Its features and functionality -> are subject to change as we collect feedback. We're excited to hear any thoughts -> you have and what features you'd like to see! +# OpenAI-Compatible Frontend for Triton Inference Server ## Pre-requisites From eb04f5f2cd7df8e9582046aa4459ee2da68ee3dc Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Thu, 18 Dec 2025 08:31:58 -0800 Subject: [PATCH 126/230] feat: Extend histogram metrics to non-decoupled models (#8580) --- docs/user_guide/metrics.md | 8 ++-- .../async_execute_decouple/1/model.py | 4 +- qa/L0_metrics/histogram_metrics_test.py | 42 ++++++++--------- qa/L0_metrics/metrics_config_test.py | 12 ++--- qa/L0_metrics/test.sh | 46 +++---------------- 5 files changed, 39 insertions(+), 73 deletions(-) diff --git a/docs/user_guide/metrics.md b/docs/user_guide/metrics.md index 5f403d7796..3d41872f42 100644 --- a/docs/user_guide/metrics.md +++ b/docs/user_guide/metrics.md @@ -1,5 +1,5 @@ -# [Triton Inference Server Release 25.11](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-25-10.html#rel-25-10) +# [Triton Inference Server Release 25.12](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-25-12.html#rel-25-12) -The Triton Inference Server container image, release 25.11, is available +The Triton Inference Server container image, release 25.12, is available on [NGC](https://ngc.nvidia.com/catalog/containers/nvidia:tritonserver) and is open source on [GitHub](https://github.com/triton-inference-server/server). Release notes can diff --git a/python/openai/README.md b/python/openai/README.md index 568fc23f36..ac0ec5e53b 100644 --- a/python/openai/README.md +++ b/python/openai/README.md @@ -46,7 +46,7 @@ docker run -it --net=host --gpus all --rm \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ -e HF_TOKEN \ - nvcr.io/nvidia/tritonserver:25.11-vllm-python-py3 + nvcr.io/nvidia/tritonserver:25.12-vllm-python-py3 ``` 2. Launch the OpenAI-compatible Triton Inference Server: @@ -355,7 +355,7 @@ Currently, OpenAI-Compatible Frontend supports loading embedding models and embe docker run -it --net=host --gpus all --rm \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ -e HF_TOKEN \ - nvcr.io/nvidia/tritonserver:25.11-vllm-python-py3 + nvcr.io/nvidia/tritonserver:25.12-vllm-python-py3 ``` 2. Launch the OpenAI-compatible Triton Inference Server: @@ -451,7 +451,7 @@ docker run -it --net=host --gpus all --rm \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ -e HF_TOKEN \ -e TRTLLM_ORCHESTRATOR=1 \ - nvcr.io/nvidia/tritonserver:24.11-trtllm-python-py3 + nvcr.io/nvidia/tritonserver:25.12-trtllm-python-py3 ``` 2. Install dependencies inside the container: diff --git a/qa/common/gen_jetson_trt_models b/qa/common/gen_jetson_trt_models index b763d2c490..edcb3748cf 100755 --- a/qa/common/gen_jetson_trt_models +++ b/qa/common/gen_jetson_trt_models @@ -34,7 +34,7 @@ # Make all generated files accessible outside of container umask 0000 # Set the version of the models -TRITON_VERSION=${TRITON_VERSION:=25.11} +TRITON_VERSION=${TRITON_VERSION:=25.12} # Set the CUDA device to use CUDA_DEVICE=${RUNNER_ID:=0} # Set TensorRT image diff --git a/qa/common/gen_qa_model_repository b/qa/common/gen_qa_model_repository index 1c0458ba58..501fc2508a 100755 --- a/qa/common/gen_qa_model_repository +++ b/qa/common/gen_qa_model_repository @@ -60,7 +60,7 @@ log_message.status "Changing working directory to the script directory to: " "${ cd ${TRITON_MDLS_BASE_SCRIPT_DIR} log_message.status "define: default values" -TRITON_VERSION=${TRITON_VERSION:=25.11} +TRITON_VERSION=${TRITON_VERSION:=25.12} ONNX_VERSION=1.16.1 ONNX_OPSET=0 OPENVINO_VERSION=2024.5.0 From f21d793d2193f215598f5e2a2ff26e204d6d3c59 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Tue, 6 Jan 2026 09:05:13 -0800 Subject: [PATCH 129/230] Update OpenVINO version to 2025.4.1 (#8588) --- LICENSE | 2 +- build.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/LICENSE b/LICENSE index d367cc74ad..6c65d3b34d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +Copyright (c) 2018-2026, NVIDIA CORPORATION. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions diff --git a/build.py b/build.py index 59575b9da5..9f7903c5a4 100755 --- a/build.py +++ b/build.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2020-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -73,10 +73,10 @@ DEFAULT_TRITON_VERSION_MAP = { "release_version": "2.65.0dev", "triton_container_version": "26.01dev", - "upstream_container_version": "25.11", + "upstream_container_version": "25.12", "ort_version": "1.23.2", - "ort_openvino_version": "2025.4.0", - "standalone_openvino_version": "2025.4.0", + "ort_openvino_version": "2025.4.1", + "standalone_openvino_version": "2025.4.1", "dcgm_version": "4.4.2-1", "vllm_version": "0.11.1", "rhel_py_version": "3.12.3", From 838f10f93451ca9a17ec625763efe98b0e527b7a Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Tue, 6 Jan 2026 10:16:07 -0800 Subject: [PATCH 130/230] fix: LICENSE file range of years (#8589) --- tools/add_copyright.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/tools/add_copyright.py b/tools/add_copyright.py index a51ffbfc64..51ae8e5d2a 100644 --- a/tools/add_copyright.py +++ b/tools/add_copyright.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -77,12 +77,10 @@ def update_and_get_license() -> str: """ Updates the copyright year in the LICENSE file if necessary and then returns its contents. + + Note: LICENSE file maintains a year range if it has an older starting year. """ - # TODO: Check if this is right - if the license file needs to have a range, - # we need to remove the range before returning the license text. - # - # License file should always have the current year. - update_copyright_year(LICENSE_PATH, disallow_range=True) + update_copyright_year(LICENSE_PATH) with open(LICENSE_PATH, "r") as license_file: return license_file.read() @@ -253,6 +251,11 @@ def rst(path): def add_copyrights(paths): for path in paths: + # Special case: LICENSE file only needs year update + if os.path.basename(path) == "LICENSE": + update_copyright_year(path) + continue + for match, handler in FILE_TYPE_HANDLERS.items(): if match(path): handler(path) @@ -370,5 +373,6 @@ def test_adding_new_copyrights(content, extension, expected): assert process_text(content, extension).startswith(expected) -def test_license_has_no_range(): - assert LICENSE_TEXT.startswith(f"Copyright (c) {current_year},") +def test_license_has_current_year(): + # LICENSE file should have the current year (either as single year or end of range) + assert f"{current_year}, NVIDIA CORPORATION" in LICENSE_TEXT From 26e9b1e415d6c707d0a8be9252d88ccd4f489371 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Thu, 8 Jan 2026 10:22:24 -0800 Subject: [PATCH 131/230] Update condition for manilinux scenarios. (#8590) --- qa/L0_backend_python/common.sh | 4 ++-- qa/L0_backend_python/env/test.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qa/L0_backend_python/common.sh b/qa/L0_backend_python/common.sh index 325ddc34eb..8b447b1713 100755 --- a/qa/L0_backend_python/common.sh +++ b/qa/L0_backend_python/common.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -59,7 +59,7 @@ install_build_deps_yum() { } install_build_deps() { - if [[ ${TRITON_RHEL} -eq "1" ]]; then + if [[ ${TRITON_RHEL} -eq "1" ]] && grep -qE 'rhel|centos|fedora' /etc/os-release; then install_build_deps_yum else install_build_deps_apt diff --git a/qa/L0_backend_python/env/test.sh b/qa/L0_backend_python/env/test.sh index 67d543c762..99c1118b80 100755 --- a/qa/L0_backend_python/env/test.sh +++ b/qa/L0_backend_python/env/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -50,7 +50,7 @@ create_conda_env "3.12" "python-3-12" conda install -c conda-forge libstdcxx-ng=14 -y TORCH_VERSION="2.8.0" conda install numpy=1.26.4 -y -if [ $TRITON_RHEL -eq 1 ]; then +if [[ ${TRITON_RHEL} -eq "1" ]] && grep -qE 'rhel|centos|fedora' /etc/os-release; then TORCH_VERISON="2.17.0" fi conda install pytorch=${TORCH_VERSION} -y From 8bbbe5b04bc3a3f08d5bb86a7b0eac898749d8e7 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Fri, 9 Jan 2026 08:44:13 -0800 Subject: [PATCH 132/230] Use TensorRT-LLM fork in Triton Inference Server development (#8593) --- qa/L0_openai/test.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qa/L0_openai/test.sh b/qa/L0_openai/test.sh index 5820b75dd0..56283af1e1 100755 --- a/qa/L0_openai/test.sh +++ b/qa/L0_openai/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -31,7 +31,7 @@ function download_tensorrt_llm_models { TENSORRTLLM_VERSION="$1" TENSORRTLLM_DIR="$2" rm -rf ${TENSORRTLLM_DIR} && mkdir ${TENSORRTLLM_DIR} - git clone --filter=blob:none --no-checkout https://github.com/NVIDIA/TensorRT-LLM.git ${TENSORRTLLM_DIR} + git clone --filter=blob:none --no-checkout https://github.com/triton-inference-server/TensorRT-LLM.git ${TENSORRTLLM_DIR} pushd ${TENSORRTLLM_DIR} git sparse-checkout set triton_backend/all_models git checkout ${TENSORRTLLM_VERSION} From ab41d5e49503b0b5c17eae2f5971eeb2cea3862c Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:38:57 -0800 Subject: [PATCH 133/230] Update default branch to track development for 2.66.0 / 26.02 (#8600) --- TRITON_VERSION | 2 +- build.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/TRITON_VERSION b/TRITON_VERSION index 361b45bd7a..189a1ca31a 100644 --- a/TRITON_VERSION +++ b/TRITON_VERSION @@ -1 +1 @@ -2.65.0dev +2.66.0dev diff --git a/build.py b/build.py index 9f7903c5a4..0ca4cb382b 100755 --- a/build.py +++ b/build.py @@ -71,8 +71,8 @@ # DEFAULT_TRITON_VERSION_MAP = { - "release_version": "2.65.0dev", - "triton_container_version": "26.01dev", + "release_version": "2.66.0dev", + "triton_container_version": "26.02dev", "upstream_container_version": "25.12", "ort_version": "1.23.2", "ort_openvino_version": "2025.4.1", From 4c8b1070e63850f8e3e1f5e1bf30401b55e3f011 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Tue, 13 Jan 2026 15:15:37 -0800 Subject: [PATCH 134/230] tests: Update torch installation (#8598) --- qa/L0_dlpack_multi_gpu/test.sh | 4 ++-- qa/L0_io/test.sh | 4 ++-- qa/L0_libtorch_instance_group_kind_model/test.sh | 4 ++-- qa/L0_warmup/test.sh | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/qa/L0_dlpack_multi_gpu/test.sh b/qa/L0_dlpack_multi_gpu/test.sh index 65dd2ad0b3..0aaf135b5f 100755 --- a/qa/L0_dlpack_multi_gpu/test.sh +++ b/qa/L0_dlpack_multi_gpu/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -41,7 +41,7 @@ source ../common/util.sh # Uninstall the non CUDA version of PyTorch pip3 uninstall -y torch -pip3 install torch==2.3.1+cu118 -f https://download.pytorch.org/whl/torch_stable.html +pip3 install torch -f https://download.pytorch.org/whl/cu130 # Install CuPy for testing non_blocking compute streams pip3 install cupy-cuda13x diff --git a/qa/L0_io/test.sh b/qa/L0_io/test.sh index 10318da4db..43c7e7dd41 100755 --- a/qa/L0_io/test.sh +++ b/qa/L0_io/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -55,7 +55,7 @@ LD_LIBRARY_PATH=/opt/tritonserver/lib:$LD_LIBRARY_PATH rm -f $CLIENT_LOG* # PyTorch is required for the Python backend dlpack add sub models -pip3 install torch==2.3.1+cu118 -f https://download.pytorch.org/whl/torch_stable.html +pip3 install torch -f https://download.pytorch.org/whl/cu130 RET=0 # Prepare float32 models with basic config diff --git a/qa/L0_libtorch_instance_group_kind_model/test.sh b/qa/L0_libtorch_instance_group_kind_model/test.sh index 9cbb6bf550..a68a8b3257 100755 --- a/qa/L0_libtorch_instance_group_kind_model/test.sh +++ b/qa/L0_libtorch_instance_group_kind_model/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -39,7 +39,7 @@ if [ ! -z "$TEST_REPO_ARCH" ]; then fi pip3 uninstall -y torch -pip3 install torch==2.3.1+cu118 -f https://download.pytorch.org/whl/torch_stable.html +pip3 install torch -f https://download.pytorch.org/whl/cu130 DATADIR=/data/inferenceserver/${REPO_VERSION}/qa_model_repository SERVER=/opt/tritonserver/bin/tritonserver diff --git a/qa/L0_warmup/test.sh b/qa/L0_warmup/test.sh index c3e6885062..84a0752229 100755 --- a/qa/L0_warmup/test.sh +++ b/qa/L0_warmup/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -420,7 +420,7 @@ wait $SERVER_PID # Test the onnx model to verify that the memory type of the output tensor # remains unchanged with the warmup setting pip3 uninstall -y torch -pip3 install torch==2.3.1+cu118 -f https://download.pytorch.org/whl/torch_stable.html +pip3 install torch -f https://download.pytorch.org/whl/cu130 rm -fr models && mkdir models cp -r /data/inferenceserver/${REPO_VERSION}/qa_model_repository/onnx_nobatch_float32_float32_float32 models/. From 50bbba60c7fda9c27d33f1dffc5ec347df3f6d5d Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Fri, 16 Jan 2026 08:06:02 -0800 Subject: [PATCH 135/230] cpu-only: inlcude libnvshmem. (#8606) --- build.py | 1 + 1 file changed, 1 insertion(+) diff --git a/build.py b/build.py index 0ca4cb382b..71791f5745 100755 --- a/build.py +++ b/build.py @@ -1583,6 +1583,7 @@ def add_cpu_libs_to_linux_dockerfile(backends, target_machine): COPY --from=min_container /usr/local/cuda/lib64/libcufile.so.0 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. COPY --from=min_container /usr/local/cuda/lib64/libnvrtc.so.13 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. COPY --from=min_container /usr/local/cuda/lib64/libcusparseLt.so.0 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. +COPY --from=min_container /usr/local/cuda/lib64/libnvshmem_host.so.3 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. RUN mkdir -p /opt/hpcx/ucc/lib/ /opt/hpcx/ucx/lib/ COPY --from=min_container /opt/hpcx/ucc/lib/libucc.so.1 /opt/hpcx/ucc/lib/libucc.so.1 From abbb4154819fcc3472aaf82ea7f8f4c5a71b9b14 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Tue, 20 Jan 2026 05:55:39 -0800 Subject: [PATCH 136/230] fix: Correct error variable pointer in Sagemaker server (#8608) --- src/sagemaker_server.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sagemaker_server.cc b/src/sagemaker_server.cc index f2cf63d276..cf379e910e 100644 --- a/src/sagemaker_server.cc +++ b/src/sagemaker_server.cc @@ -1,4 +1,4 @@ -// Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -735,7 +735,7 @@ SagemakerAPIServer::SageMakerMMEUnloadModel( server_.get(), repo_parent_path.c_str()); if (unregister_err != nullptr) { - EVBufferAddErrorJson(req->buffer_out, unload_err); + EVBufferAddErrorJson(req->buffer_out, unregister_err); evhtp_send_reply(req, EVHTP_RES_BADREQ); LOG_ERROR << "Unable to unregister model repository for path: " << repo_parent_path << std::endl; From 702c6da5b8ed6c41d3331fd39ec52ee4571afb1e Mon Sep 17 00:00:00 2001 From: J Wyman Date: Tue, 20 Jan 2026 11:13:10 -0500 Subject: [PATCH 137/230] docs: correct model management (#8562) --- docs/user_guide/model_management.md | 155 ++++++++++++++++------------ 1 file changed, 90 insertions(+), 65 deletions(-) diff --git a/docs/user_guide/model_management.md b/docs/user_guide/model_management.md index c779ddd48d..a14e978145 100644 --- a/docs/user_guide/model_management.md +++ b/docs/user_guide/model_management.md @@ -1,5 +1,5 @@ +# \ + +**Status**: \[Draft | Under Review | Approved | Replaced | Deferred | Rejected\] + +**Authors**: \[Name/Team\] + +**Category**: \[Architecture | Process | Guidelines\] + +**Replaces**: \[Link of previous proposal if applicable\] + +**Replaced By**: \[Link of previous proposal if applicable\] + +**Sponsor**: \[Name of code owner or maintainer to shepherd process\] + +**Required Reviewers**: \[Names of technical leads that are required for acceptance\] + +**Review Date**: \[Date for review\] + +**Pull Request**: \[Link to Pull Request of the Proposal itself\] + +**Implementation PR / Tracking Issue**: \[Link to Pull Request or Tracking Issue for Implementation\] + +## Summary + +**\[Required\]** + +## Motivation + +**\[Required\]** + +Describe the problem that needs to be addressed with enough detail for someone familiar with the project to understand. +Generally one to two short paragraphs. +Additional details can be placed in the background section as needed. +Cover **what** the issue is and **why** it needs to be addressed. +Link to github issues if relevant. + +### Goals + +**\[Optional \- if not applicable omit\]** + +List out any additional goals in bullet points. +Goals may be aspirational / difficult to measure but guide the proposal. + +* Goal + +* Goal + +* Goal + +### Non Goals + +**\[Optional \- if not applicable omit\]** + +List out any items which are out of scope / specifically not required in bullet points. +Indicates the scope of the proposal and issue being resolved. + +### Requirements + +**\[Optional \- if not applicable omit\]** + +List out any additional requirements in numbered subheadings. + +**\** + +#### REQ \<\#\> \ + +Describe the requirement in as much detail as necessary for others to understand it and how it applies to the TEP. +Keep in mind that requirements should be measurable and will be used to determine if a TEP has been successfully implemented or not. + +Requirement names should be prefixed using a monotonically increasing number such as “REQ 1 \” followed by “REQ 2 \” and so on. +Use title casing when naming requirements. Requirement names should be as descriptive as possible while remaining as terse as possible. + +Use all-caps, bolded terms like **MUST** and **SHOULD** when describing each requirement. +See \[RFC-2119\](https://datatracker.ietf.org/doc/html/rfc2119) for additional information. + +## Proposal + +**\[Required\]** + +Describe the high level design / proposal. +Use sub sections as needed, but start with an overview and then dig into the details. +Try to provide images and diagrams to facilitate understanding. + +## Implementation Details + +**\[Optional \- if not applicable omit\]** + +Add additional detailed items here including interface signatures, etc. +Add anything that is relevant but seems more of a detail than central to the proposal. +Use sub sections / bullet points as needed. +Try to provide images and diagrams to facilitate understanding. +If applicable link to PR. + +### Deferred to Implementation + +**\[Optional \- if not applicable omit\]** + +List out items that are under discussion but that will be resolved only during implementation / code review. + +## Implementation Phases + +**\[Optional \- if not applicable omit\]** + +List out phases of implementation (can be single phase). +Give each phase a monotonically increasing number; example “Phase 0” followed by “Phase 1” and so on. +Give phases titles if it makes sense. + +### Phase \<\#\> \ + +**Release Target**: Date + +**Effort Estimate**: \ + +**Work Item(s):** \ + +**Supported API / Behavior:** + +* \ + +**Not Supported:** + +* \ + +## Related Proposals + +**\[Optional \- if not applicable omit\]** + +* File + +* File + +* File + +* File + +* File + +## Alternate Solutions + +**\[Required, if not applicable write N/A\]** + +List out solutions that were considered but ultimately rejected. +Consider free form `-`, but a possible format shown below. + +### Alt \<\#\> \ + +**Pros:** + +\ + +**Cons:** + +\ + +**Reason Rejected:** + +\ + +**Notes:** + +\ + +## Background + +**\[Optional \- if not applicable omit\]** + +Add additional context and references as needed to help reviewers and authors understand the context of the problem and solution being proposed. + +## References + +**\[Optional \- if not applicable omit\]** + +Add additional references as needed to help reviewers and authors understand the context of the problem and solution being proposed. + +* \ + +## Terminology & Definitions + +**\[Optional \- if not applicable omit\]** + +List out additional terms / definitions (lexicon). +Try to keep definitions as concise as possible and use links to external resources when additional information would be useful to the reader. + +Keep the list of terms sorted alphabetically to ease looking up definitions by readers. + +| \ | \ | +| :---- | :---- | +| **\** | \ | + +## Acronyms & Abbreviations + +**\[Optional \- if not applicable omit\]** + +Provide a list of frequently used acronyms and abbreviations which are uncommon or unlikely to be known by the reader. +Do not include acronyms or abbreviations which the reader is likely to be familiar with. + +Keep the list of acronyms and abbreviations sorted alphabetically to ease looking up definitions by readers. + +Do not include the full definition in the expanded meaning of an abbreviation or acronym. +If the reader needs the definition, please include it in the \[Terminology & Definitions\](#terminology--definitions) section. + +**\:** \ diff --git a/enhancements/NNNN-template-limited.md b/enhancements/NNNN-template-limited.md new file mode 100644 index 0000000000..f814dce593 --- /dev/null +++ b/enhancements/NNNN-template-limited.md @@ -0,0 +1,134 @@ + +# \ + +**Status**: \[Draft | Under Review | Approved | Replaced | Deferred | Rejected\] + +**Authors**: \[Name/Team\] + +**Category**: \[Architecture | Process | Guidelines\] + +**Replaces**: \[Link of previous proposal if applicable\] + +**Replaced By**: \[Link of previous proposal if applicable\] + +**Sponsor**: \[Name of code owner or maintainer to shepherd process\] + +**Required Reviewers**: \[Names of technical leads that are required for acceptance\] + +**Review Date**: \[Date for review\] + +**Pull Request**: \[Link to Pull Request of the Proposal itself\] + +**Implementation PR / Tracking Issue**: \[Link to Pull Request or Tracking Issue for Implementation\] + +## Summary + +**\[Required\]** + +## Motivation + +**\[Required\]** + +Describe the problem that needs to be addressed with enough detail for someone familiar with the project to understand. +Generally one to two short paragraphs. +Additional details can be placed in the background section as needed. Cover **what** the issue is and **why** it needs to be addressed. +Link to github issues if relevant. + +### Goals + +**\[Optional \- if not applicable omit\]** + +List out any additional goals in bullet points. +Goals may be aspirational / difficult to measure but guide the proposal. + +* Goal + +* Goal + +* Goal + +#### Non Goals + +**\[Optional \- if not applicable omit\]** + +List out any items which are out of scope / specifically not required in bullet points. +Indicates the scope of the proposal and issue being resolved. + +### Requirements + +**\[Optional \- if not applicable omit\]** + +List out any additional requirements in numbered subheadings. + +**\** + +#### REQ \<\#\> \ + +Describe the requirement in as much detail as necessary for others to understand it and how it applies to the TEP. +Keep in mind that requirements should be measurable and will be used to determine if a TEP has been successfully implemented or not. + +Requirement names should be prefixed using a monotonically increasing number such as “REQ 1 \” followed by “REQ 2 \” and so on. +Use title casing when naming requirements. +Requirement names should be as descriptive as possible while remaining as terse as possible. + +Use all-caps, bolded terms like **MUST** and **SHOULD** when describing each requirement. +See \[RFC-2119\](https://datatracker.ietf.org/doc/html/rfc2119) for additional information. + +## Proposal + +**\[Required\]** + +Describe the high level design / proposal. +Use sub sections as needed, but start with an overview and then dig into the details. +Try to provide images and diagrams to facilitate understanding. + +## Alternate Solutions + +**\[Required, if not applicable write N/A\]** + +List out solutions that were considered but ultimately rejected. +Consider free form `-`, but a possible format shown below. + +## Alt \<\#\> \ + +**Pros:** + +\ + +**Cons:** + +\ + +**Reason Rejected:** + +\ + +**Notes:** + +\ diff --git a/enhancements/README.md b/enhancements/README.md new file mode 100644 index 0000000000..6cf8c3a1da --- /dev/null +++ b/enhancements/README.md @@ -0,0 +1,44 @@ + +# Triton Enhancement Proposals (TEP) + +Enhancement Proposals and Architecture Decisions + +Please see [0000-tep-process](teps/0000-tep-process.md) for full explanation and details. + +## Authoring Guidelines + +1. Start with either the: +- [NNNN-template-complete.md](NNNN-template-complete.md) and remove unneeded sections. +- [NNNN-template-limited.md](NNNN-template-limited.md) and then add selectively from the complete template based on need. + +1. Identify a **Code-Owner** or **Maintainer** of the TEP repository to shepherd the process. + +2. Create a draft PR and iterate with co-authors, **Sponsor** + +3. When ready for review, mark as ready and work with **Sponsor** to set a **Review Date**. diff --git a/enhancements/teps/0000-tep-process.md b/enhancements/teps/0000-tep-process.md new file mode 100644 index 0000000000..b85c538dcf --- /dev/null +++ b/enhancements/teps/0000-tep-process.md @@ -0,0 +1,265 @@ + +# Triton Enhancement Proposals + +**Status**: Draft + +**Authors**: [whoisj](https://github.com/whoisj) + +**Category**: Process + +**Replaces**: N/A + +**Replaced By**: N/A + +**Sponsor**: [whoisj](https://github.com/whoisj) + +**Required Reviewers**: [dzier](https://github.com/dzier), [nnshah1](https://github.com/nnshah1) + +**Review Date**: 17 Oct 2025 + +**Pull Request**: [N/A](https://github.com/triton-inference-server/server/pull/8517) + +## Summary + +A standard process and format for proposing and capturing architecture, design, and process decisions for the Triton project along with the motivations behind those decisions. +We adopt a similar process as adopted by Dynamo, Kubernetes, Rust, Python, and Ray broadly categorized as "enhancement proposals". + +## Motivation + +With any software project but especially agile, open source projects in the AI space, architecture, design, and process decisions are made rapidly and for specific reasons which can sometimes be difficult to understand after the fact. +For Triton in particular many teams and community members are collaborating for the first time and have varied backgrounds and design philosophies. +The Triton project's code base itself reflects multiple previously independent code bases integrated quickly to meet overall project goals. +As the project evolves we need a way to propose, ratify and capture architecture, design and process decisions quickly and thoughtfully in a transparent, consistent, lightweight, maintainable way. + +Borrowing from the motivation for KEPs: + +> The purpose of the KEP process is to reduce the amount of "tribal knowledge" in our community. +> By moving decisions from a smattering of mailing lists, video calls and hallway conversations into a well tracked artifact, this process aims to enhance communication and discoverability. + +### Goals + +* **Useful** + + Enhancement proposals and the process of writing and approving them should encourage the thoughtful evaluation of design, process, and architecture choices and lead to timely decisions with a clear record of what was decided, why, and what other options were considered. + +* **Lightweight and Scalable** + + The format and process should be applicable both to small or medium sized changes as well as large ones. + The process should not impede the rate of progress but serve to provide timely feedback, discussion, and ratification on key proposals. + The process should also support retroactive documents to capture and explain decisions already made. + +* **Single Document for Requirements and Design** + + Combine aspects of requirements documents, design documents and software architecture documents into a single document. + Give one place to understand the motivation, requirements, and design of a feature or process. + +* **Support Process, Architecture and Guideline Decisions** + + Have a single format to articulate decisions that effect process (such as github merge rules or templates) as well as code and design guidelines as well as features. + +* **Clear** + + Should be relatively clear when a document is required, when the review needs to be completed, and by who and what the overall process is. + +* **Encourage Collaboration** + + Should allow for easy collaboration and communication between *Authors** and **Reviewers**. + +* **Flexible** + + Format and process should be flexible enough to be used for different types of decisions requiring different levels of detail and formatting of sections. + +### Non Goals + +* Triton Enhancement Proposals (TEP)s do not take the place of other forms of documentation such as user / developer facing documentation (including architecture documents, api documentation) +* Prototyping and early development are not gated by design / architectural approval. +* TEPs should not be a perfunctory process but lead to discussion and thought process around good designs. +* Not all changes (bug fixes, documentation improvements) need a TEP - and many can be reviewed via that normal GitHub pull request + +## Proposal + +Following successful open source projects such as Kubernetes (KEP) and Dynamo (DEP) we adopt a markdown based enhancement proposal format designed to support any decisions we need to capture as a project. +We will adopt an open, community-wide, discussion and comment process using pull requests but enable **Code-Owners** and **Maintainers** to be the final arbiters of **Approval**. + +Subject area experts will be listed as required **Reviewers** to ensure proposals are complete and reviewed properly. + + + +We provide two templates "limited" and "complete" where the limited template is a strict subset of the complete template, and both indicate which sections are required and which are optional. + +## Implementation Details + +### Proposal Process + + + +* Copy the [limited template](../NNNN-template-limited.md) or [complete template](../NNNN-template-complete.md) to `teps/NNNN-my-feature.md` (where `my-feature` is descriptive, don't assign an `TEP` identifier yet) + + > [!Note] + > Choose the template that fits your purpose. + > You can start with the limited form and pull additional sections from the complete form as needed. + > Keep the order of the sections consistent. + +* Identify a **Sponsor** from the list of **Maintainers** or **Code-Owners** to help with the process. + +* Fill in the proposal template. + Be sure to include all required sections. + Keep sections in the order prescribed in the template. + +* Work with the **Sponsor** to identify the required reviewers and a timeline for review. + + + +* If discussion is needed the **Sponsor** can ask for a slot in the weekly Engineering Sync or schedule an ad-hoc meeting with the required reviewers. + +* Iterate and incorporate feedback via the pull request. + +* When review is complete The **Sponsor** will merge the request and update the status. + +* **Sponsor** should assign an identifier. + +* **Author** and **Sponsor** should add issues and/or PRs as needed to track implementation. + +### When is a proposal required? + +It is difficult to enumerate all the circumstances where a proposal would be required or not required. +Generally we will follow this process when making "substantial changes". +The definition of "substantial" is evolving and mainly determined by the core team and community. + +When in doubt reach out to a **Maintainer** or **Code-Owner**. + +**Generally speaking a proposal would not be required for**: + +* Bug fixes that don't change advertised behavior + +* Documentation fixes / updates + +* Minor refactors within a single module + +**Generally speaking proposals would be required for**: + +* New features which add significant functionality + +* Changes to existing features or code which require discussion + +* Changes to public interfaces + +* Responses to security related vulnerabilities found directly in the project code + +* Changes to packaging and installation + +* When a **Maintainer** or **Code-Owner** recommends that a change go through the proposal process + +* Retroactively to capture current architecture, guideline, or process + +### Minor Changes After Review + +For minor changes or changes that are in the spirit of the review, updates can be made to the document without a new proposal. + +*Example:* links to implementation + +### Significant Changes After Review + +For significant changes, a new proposal should be made and the original marked as replaced. + +### Maintenance + +TEPs should be reviewed for updates, replacements, or archiving on a regular basis. + +### Sensitive Changes and Discussions + +Certain types of changes need to be discussed and ratified before being made public due to timing of non-disclosed information. +In such (rare) cases, drafts and reviews will be conducted offline by **Authors**, **Code-Owners**, and **Maintainers** with the public proposals being updated when possible. + +*Example:* when responding to undisclosed security vulnerabilities, we want to avoid inadvertently encouraging zero day attacks for deployed systems. + +In such (rare) cases, we may make use of a private repo on a temporary basis to collect feedback before publishing to the public repo. + +### Deferred to Implementation + +* Definition of **Code-Owners** and **Maintainers** + +* Whether or not to organize **TEP**s into sub directories for projects / areas + +* Tooling around the creation / indexing of **TEP**s + +* Making requirements required in addition to motivation + +* Format recommendations for API surfaces / other formatted components. + +* Decisions / guidelines on when a TEP is needed. + +## Alternate Solutions + +### Alt 1 Google Docs + +**Pros:** + +* Fits existing documents and templates used by many teams + +**Cons:** + +* Difficult to integrate with AI tools. + +* Difficult to search and index + +**Reason Rejected:** + +* Want to standardize around a simple text format and use AI tools also for diagramming, etc. + +## Background + +With the rise of Agile software development practices and large open source projects, software development teams needed to devise new and lightweight (w.r.t to previous software architecture documents) ways of recording architecture proposals and decisions. +As Agile was born in part as a reaction to waterfall styles of planning and development and famously prioritized “Working software over comprehensive documentation”, so too there was a need to replace monolithic large software design specifications with something lighter weight but that still encouraged good architecture. + +From this need for a new way of practicing software architecture a body of work and theory has evolved around the concepts of “Architecture Decision Records” which in turn are also termed “Any Decision Record”, and RFCs or Enhancement proposals (PEP, KEP, REP). + +In each case the core requirements of the process are that the team document the problem, the proposal / design, the status of the proposal, implications / follow on work, and any alternatives that were considered using a standard template and review process. + +Just as in Agile planning, each team modifies the template and process to fit their needs. + +### References + +1. [Documenting Architecture Decisions (cognitect.com)](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions) + +2. [The most plagiarized Architecture Decision Record blog on the internet. | by Conall Daly | Medium](https://conalldalydev.medium.com/the-most-plagiarised-architecture-decision-record-blog-on-the-internet-c9dd2018c1d6) + +3. [adr.github.io](https://adr.github.io/) + +4. [When Should I Write an Architecture Decision Record \- Spotify Engineering : Spotify Engineering (atspotify.com)](https://engineering.atspotify.com/2020/04/when-should-i-write-an-architecture-decision-record/) + +5. [Scaling Engineering Teams via RFCs: Writing Things Down \- The Pragmatic Engineer](https://blog.pragmaticengineer.com/scaling-engineering-teams-via-writing-things-down-rfcs/) + +6. [Love Unrequited: The Story of Architecture, Agile, and How Architecture Decision Records Brought Them Together | IEEE Journals & Magazine | IEEE Xplore](https://ieeexplore.ieee.org/document/9801811) + +7. [ray-project/enhancements: Tracking Ray Enhancement Proposals (github.com)](https://github.com/ray-project/enhancements) + +8. [Kubernetes Enhancement Proposals](https://github.com/kubernetes/enhancements/blob/master/keps/sig-architecture/0000-kep-process/README.md) + +9. [Dynamo Enhancement Proposals](https://github.com/ai-dynamo/enhancements/blob/main/README.md) From b1bacfc0d7493e49b478e777bcbe1928704afe03 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Fri, 30 Jan 2026 00:04:08 -0800 Subject: [PATCH 144/230] ci: Fix L0_model_control_stress (#8627) --- qa/python_models/torchvision/resnet50/model.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/qa/python_models/torchvision/resnet50/model.py b/qa/python_models/torchvision/resnet50/model.py index 6a31a77420..46f83bcafd 100644 --- a/qa/python_models/torchvision/resnet50/model.py +++ b/qa/python_models/torchvision/resnet50/model.py @@ -1,4 +1,4 @@ -# Copyright 2023-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -25,6 +25,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import torch +import torchvision import triton_python_backend_utils as pb_utils from torch.utils.dlpack import to_dlpack @@ -35,14 +36,9 @@ def initialize(self, args): This function initializes pre-trained ResNet50 model. """ self.device = "cuda" if args["model_instance_kind"] == "GPU" else "cpu" - # Avoid the "HTTP Error 403: rate limit exceeded" error - torch.hub._validate_not_a_forked_repo = lambda a, b, c: True - # Our tests currently depend on torchvision=0.14, - # to make sure `torch.hub` loads Resnet50 implementation - # compatible with torchvision=0.14, we need to provide tag self.model = ( - torch.hub.load( - "pytorch/vision:v0.14.1", "resnet50", weights="IMAGENET1K_V2" + torchvision.models.resnet50( + weights=torchvision.models.ResNet50_Weights.DEFAULT ) .to(self.device) .eval() From 1307ace3ad489ea9087d8f7c2d13839e5d0317aa Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Fri, 30 Jan 2026 11:33:36 -0800 Subject: [PATCH 145/230] fix: Use correct torchvision model weights (#8628) --- qa/python_models/torchvision/resnet50/model.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/qa/python_models/torchvision/resnet50/model.py b/qa/python_models/torchvision/resnet50/model.py index 46f83bcafd..0e6405d302 100644 --- a/qa/python_models/torchvision/resnet50/model.py +++ b/qa/python_models/torchvision/resnet50/model.py @@ -25,9 +25,9 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import torch -import torchvision import triton_python_backend_utils as pb_utils from torch.utils.dlpack import to_dlpack +from torchvision import models class TritonPythonModel: @@ -37,9 +37,7 @@ def initialize(self, args): """ self.device = "cuda" if args["model_instance_kind"] == "GPU" else "cpu" self.model = ( - torchvision.models.resnet50( - weights=torchvision.models.ResNet50_Weights.DEFAULT - ) + models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2) .to(self.device) .eval() ) From 265d5cb81e84f16d94e0607d3bec953e241d845f Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Tue, 3 Feb 2026 12:18:08 -0800 Subject: [PATCH 146/230] post: Update default branch post-26.01 (#8633) Co-authored-by: Yingge He <157551214+yinggeh@users.noreply.github.com> --- Dockerfile.sdk | 4 +-- README.md | 12 +++---- build.py | 9 ++--- deploy/aws/values.yaml | 4 +-- deploy/fleetcommand/Chart.yaml | 4 +-- deploy/fleetcommand/values.yaml | 8 ++--- deploy/gcp/values.yaml | 4 +-- .../perf-analyzer-script/triton_client.yaml | 4 +-- .../server-deployer/build_and_push.sh | 8 ++--- .../server-deployer/chart/triton/Chart.yaml | 6 ++-- .../server-deployer/chart/triton/values.yaml | 8 ++--- .../server-deployer/data-test/schema.yaml | 4 +-- .../server-deployer/schema.yaml | 6 ++-- .../gke-marketplace-app/trt-engine/README.md | 8 ++--- deploy/k8s-onprem/values.yaml | 4 +-- deploy/oci/values.yaml | 4 +-- docs/client_guide/python_readme.rst | 36 +++++++++++++++++-- docs/introduction/compatibility.md | 5 ++- docs/introduction/release_notes.md | 6 ++-- python/openai/README.md | 8 ++--- .../openai_frontend/engine/triton_engine.py | 7 +++- .../openai_frontend/engine/utils/triton.py | 4 ++- python/openai/requirements.txt | 5 ++- python/openai/tests/conftest.py | 3 +- python/openai/tests/test_completions.py | 16 ++++++--- qa/common/gen_jetson_trt_models | 2 +- qa/common/gen_qa_model_repository | 2 +- 27 files changed, 120 insertions(+), 71 deletions(-) diff --git a/Dockerfile.sdk b/Dockerfile.sdk index 59bdaea509..8041286307 100644 --- a/Dockerfile.sdk +++ b/Dockerfile.sdk @@ -1,4 +1,4 @@ -# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -29,7 +29,7 @@ # # Base image on the minimum Triton container -ARG BASE_IMAGE=nvcr.io/nvidia/tritonserver:25.12-py3-min +ARG BASE_IMAGE=nvcr.io/nvidia/tritonserver:26.01-py3-min ARG TRITON_CLIENT_REPO_SUBDIR=clientrepo ARG TRITON_REPO_ORGANIZATION=http://github.com/triton-inference-server diff --git a/README.md b/README.md index 522927495c..03b498e561 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# [Triton Inference Server Release 25.12](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-25-12.html#rel-25-12) +# [Triton Inference Server Release 26.01](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-26-01.html#rel-26-01) -The Triton Inference Server container image, release 25.12, is available +The Triton Inference Server container image, release 26.01, is available on [NGC](https://ngc.nvidia.com/catalog/containers/nvidia:tritonserver) and is open source on [GitHub](https://github.com/triton-inference-server/server). Release notes can diff --git a/python/openai/README.md b/python/openai/README.md index ac0ec5e53b..ed770b4e05 100644 --- a/python/openai/README.md +++ b/python/openai/README.md @@ -1,5 +1,5 @@ inference -> data postprocessing". Using ensemble models for this -purpose can avoid the overhead of transferring intermediate tensors -and minimize the number of requests that must be sent to Triton. - -The ensemble scheduler must be used for ensemble models, regardless of -the scheduler used by the models within the ensemble. With respect to -the ensemble scheduler, an *ensemble* model is not an actual -model. Instead, it specifies the dataflow between models within the -ensemble as *ModelEnsembling::Step* entries in the model -configuration. The scheduler collects the output tensors in each step, -provides them as input tensors for other steps according to the -specification. In spite of that, the ensemble model is still viewed as -a single model from an external view. - -Note that the ensemble models will inherit the characteristics of the -models involved, so the meta-data in the request header must comply -with the models within the ensemble. For instance, if one of the -models is stateful model, then the inference request for the ensemble -model should contain the information mentioned in [Stateful -Models](#stateful-models), which will be provided to the stateful -model by the scheduler. - -As an example consider an ensemble model for image classification and -segmentation that has the following model configuration: - -``` -name: "ensemble_model" -platform: "ensemble" -max_batch_size: 1 -input [ - { - name: "IMAGE" - data_type: TYPE_STRING - dims: [ 1 ] - } -] -output [ - { - name: "CLASSIFICATION" - data_type: TYPE_FP32 - dims: [ 1000 ] - }, - { - name: "SEGMENTATION" - data_type: TYPE_FP32 - dims: [ 3, 224, 224 ] - } -] -ensemble_scheduling { - step [ - { - model_name: "image_preprocess_model" - model_version: -1 - input_map { - key: "RAW_IMAGE" - value: "IMAGE" - } - output_map { - key: "PREPROCESSED_OUTPUT" - value: "preprocessed_image" - } - }, - { - model_name: "classification_model" - model_version: -1 - input_map { - key: "FORMATTED_IMAGE" - value: "preprocessed_image" - } - output_map { - key: "CLASSIFICATION_OUTPUT" - value: "CLASSIFICATION" - } - }, - { - model_name: "segmentation_model" - model_version: -1 - input_map { - key: "FORMATTED_IMAGE" - value: "preprocessed_image" - } - output_map { - key: "SEGMENTATION_OUTPUT" - value: "SEGMENTATION" - } - } - ] -} -``` - -The ensemble\_scheduling section indicates that the ensemble scheduler will be -used and that the ensemble model consists of three different models. Each -element in step section specifies the model to be used and how the inputs and -outputs of the model are mapped to tensor names recognized by the scheduler. For -example, the first element in step specifies that the latest version of -image\_preprocess\_model should be used, the content of its input "RAW\_IMAGE" -is provided by "IMAGE" tensor, and the content of its output -"PREPROCESSED\_OUTPUT" will be mapped to "preprocessed\_image" tensor for later -use. The tensor names recognized by the scheduler are the ensemble inputs, the -ensemble outputs and all values in the input\_map and the output\_map. - -The models composing the ensemble may also have dynamic batching -enabled. Since ensemble models are just routing the data between -composing models, Triton can take requests into an ensemble model -without modifying the ensemble's configuration to exploit the dynamic -batching of the composing models. - -Assuming that only the ensemble model, the preprocess model, the classification -model and the segmentation model are being served, the client applications will -see them as four different models which can process requests independently. -However, the ensemble scheduler will view the ensemble model as the following. - -![Ensemble Example](images/ensemble_example0.png) - -When an inference request for the ensemble model is received, the ensemble -scheduler will: - -1. Recognize that the "IMAGE" tensor in the request is mapped to input - "RAW\_IMAGE" in the preprocess model. - -2. Check models within the ensemble and send an internal request to the - preprocess model because all the input tensors required are ready. - -3. Recognize the completion of the internal request, collect the output - tensor and map the content to "preprocessed\_image" which is an unique name - known within the ensemble. - -4. Map the newly collected tensor to inputs of the models within the ensemble. - In this case, the inputs of "classification\_model" and "segmentation\_model" - will be mapped and marked as ready. - -5. Check models that require the newly collected tensor and send internal - requests to models whose inputs are ready, the classification - model and the segmentation model in this case. Note that the responses will - be in arbitrary order depending on the load and computation time of - individual models. - -6. Repeat step 3-5 until no more internal requests should be sent, and then - response to the inference request with the tensors mapped to the ensemble - output names. - -Unlike other models, ensemble models do not support "instance_group" field in -the model configuration. The reason is that the ensemble scheduler itself -is mainly an event-driven scheduler with very minimal overhead so its -almost never the bottleneck of the pipeline. The composing models -within the ensemble can be individually scaled up or down with their -respective `instance_group` settings. To optimize your model pipeline -performance, you can use -[Model Analyzer](https://github.com/triton-inference-server/model_analyzer) -to find the optimal model configurations. - -When crafting the ensemble steps, it is useful to note the distinction between -*key* and *value* on the `input_map`/`output_map`: -* *key*: An `input`/`output` tensor name on the composing model. -* *value*: A tensor name on the ensemble model, which acts as an identifier -connecting ensemble `input`/`output` to those on the composing model and between -composing models. - -#### Additional Resources - -You can find additional end-to-end ensemble examples in the links below: -* [This guide](https://github.com/triton-inference-server/tutorials/tree/main/Conceptual_Guide/Part_5-Model_Ensembles) -explores the concept of ensembles with a running example. -* [Preprocessing in Python Backend Using - Ensemble](https://github.com/triton-inference-server/python_backend#preprocessing) -* [Accelerating Inference with NVIDIA Triton Inference Server and NVIDIA - DALI](https://developer.nvidia.com/blog/accelerating-inference-with-triton-inference-server-and-dali/) -* [Using RAPIDS AI with NVIDIA Triton Inference - Server](https://github.com/rapidsai/rapids-examples/tree/main/rapids_triton_example) - +![Triton Architecture Diagram](images/arch.jpg) \ No newline at end of file diff --git a/docs/user_guide/batcher.md b/docs/user_guide/batcher.md index 556412e455..0cb2589a1f 100644 --- a/docs/user_guide/batcher.md +++ b/docs/user_guide/batcher.md @@ -1,5 +1,5 @@ - -Installation ------------- - -The easiest way to install GenAI-Perf is through `Triton Server SDK -container `__. -Install the latest release using the following command: - -.. code:: bash - - export RELEASE="yy.mm" # e.g. export RELEASE="24.06" - - docker run -it --net=host --gpus=all nvcr.io/nvidia/tritonserver:${RELEASE}-py3-sdk - - # Check out genai_perf command inside the container: - genai-perf --help - -.. raw:: html - -
- -Alternatively, to install from source: - -Since GenAI-Perf depends on Perf Analyzer, you’ll need to install the -Perf Analyzer binary: - -Install Perf Analyzer (Ubuntu, Python 3.8+) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**NOTE**: you must already have CUDA 12 installed (checkout the `CUDA -installation -guide `__). - -.. code:: bash - - pip install tritonclient - - apt update && apt install -y --no-install-recommends libb64-0d libcurl4 - -You can also build Perf Analyzer `from -source <../docs/install.md#build-from-source>`__ as well. - -Install GenAI-Perf from source -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code:: bash - - git clone https://github.com/triton-inference-server/perf_analyzer.git && cd perf_analyzer - - pip install -e genai-perf - -.. raw:: html - -
- -.. raw:: html - - - -Quick Start ------------ - -In this quick start, we will use GenAI-Perf to run performance -benchmarking on the GPT-2 model running on Triton Inference Server with -a TensorRT-LLM engine. - -Serve GPT-2 TensorRT-LLM model using Triton CLI -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can follow the `quickstart -guide `__ -on Triton CLI github repo to run GPT-2 model locally. The full -instructions are copied below for convenience: - -.. code:: bash - - # This container comes with all of the dependencies for building TRT-LLM engines - # and serving the engine with Triton Inference Server. - docker run -ti \ - --gpus all \ - --network=host \ - --shm-size=1g --ulimit memlock=-1 \ - -v /tmp:/tmp \ - -v ${HOME}/models:/root/models \ - -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ - nvcr.io/nvidia/tritonserver:24.05-trtllm-python-py3 - - # Install the Triton CLI - pip install git+https://github.com/triton-inference-server/triton_cli.git@0.0.8 - - # Build TRT LLM engine and generate a Triton model repository pointing at it - triton remove -m all - triton import -m gpt2 --backend tensorrtllm - - # Start Triton pointing at the default model repository - triton start - -Running GenAI-Perf -~~~~~~~~~~~~~~~~~~ - -Now we can run GenAI-Perf from Triton Inference Server SDK container: - -.. code:: bash - - export RELEASE="yy.mm" # e.g. export RELEASE="24.06" - - docker run -it --net=host --rm --gpus=all nvcr.io/nvidia/tritonserver:${RELEASE}-py3-sdk - - # Run GenAI-Perf in the container: - genai-perf profile \ - -m gpt2 \ - --service-kind triton \ - --backend tensorrtllm \ - --num-prompts 100 \ - --random-seed 123 \ - --synthetic-input-tokens-mean 200 \ - --synthetic-input-tokens-stddev 0 \ - --streaming \ - --output-tokens-mean 100 \ - --output-tokens-stddev 0 \ - --output-tokens-mean-deterministic \ - --tokenizer hf-internal-testing/llama-tokenizer \ - --concurrency 1 \ - --measurement-interval 4000 \ - --profile-export-file my_profile_export.json \ - --url localhost:8001 - -Example output: - -:: - - LLM Metrics - ┏━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┓ - ┃ Statistic ┃ avg ┃ min ┃ max ┃ p99 ┃ p90 ┃ p75 ┃ - ┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━┩ - │ Time to first token (ms) │ 11.70 │ 9.88 │ 17.21 │ 14.35 │ 12.01 │ 11.87 │ - │ Inter token latency (ms) │ 1.46 │ 1.08 │ 1.89 │ 1.87 │ 1.62 │ 1.52 │ - │ Request latency (ms) │ 161.24 │ 153.45 │ 200.74 │ 200.66 │ 179.43 │ 162.23 │ - │ Output sequence length │ 103.39 │ 95.00 │ 134.00 │ 120.08 │ 107.30 │ 105.00 │ - │ Input sequence length │ 200.01 │ 200.00 │ 201.00 │ 200.13 │ 200.00 │ 200.00 │ - └──────────────────────────┴────────┴────────┴────────┴────────┴────────┴────────┘ - Output token throughput (per sec): 635.61 - Request throughput (per sec): 6.15 - -See `Tutorial `__ for additional examples. - -.. raw:: html - - - -Visualization -------------- - -GenAI-Perf can also generate various plots that visualize the -performance of the current profile run. This is disabled by default but -users can easily enable it by passing the ``--generate-plots`` option -when running the benchmark: - -.. code:: bash - - genai-perf profile \ - -m gpt2 \ - --service-kind triton \ - --backend tensorrtllm \ - --streaming \ - --concurrency 1 \ - --generate-plots - -This will generate a `set of default -plots `__ such as: - Time to first token -(TTFT) analysis - Request latency analysis - TTFT vs Input sequence -lengths - Inter token latencies vs Token positions - Input sequence -lengths vs Output sequence lengths - -Using ``compare`` Subcommand to Visualize Multiple Runs -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The ``compare`` subcommand in GenAI-Perf facilitates users in comparing -multiple profile runs and visualizing the differences through plots. - -Usage -^^^^^ - -Assuming the user possesses two profile export JSON files, namely -``profile1.json`` and ``profile2.json``, they can execute the -``compare`` subcommand using the ``--files`` option: - -.. code:: bash - - genai-perf compare --files profile1.json profile2.json - -Executing the above command will perform the following actions under the -``compare`` directory: 1. Generate a YAML configuration file -(e.g. ``config.yaml``) containing the metadata for each plot generated -during the comparison process. 2. Automatically generate the `default -set of plots `__ (e.g. TTFT vs. Input -Sequence Lengths) that compare the two profile runs. - -:: - - compare - ├── config.yaml - ├── distribution_of_input_sequence_lengths_to_output_sequence_lengths.jpeg - ├── request_latency.jpeg - ├── time_to_first_token.jpeg - ├── time_to_first_token_vs_input_sequence_lengths.jpeg - ├── token-to-token_latency_vs_output_token_position.jpeg - └── ... - -Customization -^^^^^^^^^^^^^ - -Users have the flexibility to iteratively modify the generated YAML -configuration file to suit their specific requirements. They can make -alterations to the plots according to their preferences and execute the -command with the ``--config`` option followed by the path to the -modified configuration file: - -.. code:: bash - - genai-perf compare --config compare/config.yaml - -This command will regenerate the plots based on the updated -configuration settings, enabling users to refine the visual -representation of the comparison results as per their needs. - -See `Compare documentation `__ for more details. - -.. raw:: html - - - -Model Inputs ------------- - -GenAI-Perf supports model input prompts from either synthetically -generated inputs, or from the HuggingFace -`OpenOrca `__ or -`CNN_DailyMail `__ -datasets. This is specified using the ``--input-dataset`` CLI option. - -When the dataset is synthetic, you can specify the following options: \* -``--num-prompts ``: The number of unique prompts to generate as -stimulus, >= 1. \* ``--synthetic-input-tokens-mean ``: The mean of -number of tokens in the generated prompts when using synthetic data, >= -1. \* ``--synthetic-input-tokens-stddev ``: The standard deviation -of number of tokens in the generated prompts when using synthetic data, ->= 0. \* ``--random-seed ``: The seed used to generate random -values, >= 0. - -When the dataset is coming from HuggingFace, you can specify the -following options: \* ``--input-dataset {openorca,cnn_dailymail}``: -HuggingFace dataset to use for benchmarking. \* ``--num-prompts ``: -The number of unique prompts to generate as stimulus, >= 1. - -When the dataset is coming from a file, you can specify the following -options: \* ``--input-file ``: The input file containing the -prompts to use for benchmarking as JSON objects. - -For any dataset, you can specify the following options: \* -``--output-tokens-mean ``: The mean number of tokens in each -output. Ensure the ``--tokenizer`` value is set correctly, >= 1. \* -``--output-tokens-stddev ``: The standard deviation of the number -of tokens in each output. This is only used when output-tokens-mean is -provided, >= 1. \* ``--output-tokens-mean-deterministic``: When using -``--output-tokens-mean``, this flag can be set to improve precision by -setting the minimum number of tokens equal to the requested number of -tokens. This is currently supported with the Triton service-kind. Note -that there is still some variability in the requested number of output -tokens, but GenAi-Perf attempts its best effort with your model to get -the right number of output tokens. - -You can optionally set additional model inputs with the following -option: \* ``--extra-inputs :``: An additional input -for use with the model with a singular value, such as ``stream:true`` or -``max_tokens:5``. This flag can be repeated to supply multiple extra -inputs. - -For `Large Language Models `__, there is no batch size -(i.e. batch size is always ``1``). Each request includes the inputs for -one individual inference. Other modes such as the -`embeddings `__ and `rankings `__ -endpoints support client-side batching, where ``--batch-size N`` means -that each request sent will include the inputs for ``N`` separate -inferences, allowing them to be processed together. - -.. raw:: html - - - -Metrics -------- - -GenAI-Perf collects a diverse set of metrics that captures the -performance of the inference server. - -+-----------------------+-----------------------+-----------------------+ -| Metric | Description | Aggregations | -+=======================+=======================+=======================+ -| Time to First Token | Time between when a | Avg, min, max, p99, | -| | request is sent and | p90, p75 | -| | when its first | | -| | response is received, | | -| | one value per request | | -| | in benchmark | | -+-----------------------+-----------------------+-----------------------+ -| Inter Token Latency | Time between | Avg, min, max, p99, | -| | intermediate | p90, p75 | -| | responses for a | | -| | single request | | -| | divided by the number | | -| | of generated tokens | | -| | of the latter | | -| | response, one value | | -| | per response per | | -| | request in benchmark | | -+-----------------------+-----------------------+-----------------------+ -| Request Latency | Time between when a | Avg, min, max, p99, | -| | request is sent and | p90, p75 | -| | when its final | | -| | response is received, | | -| | one value per request | | -| | in benchmark | | -+-----------------------+-----------------------+-----------------------+ -| Output Sequence | Total number of | Avg, min, max, p99, | -| Length | output tokens of a | p90, p75 | -| | request, one value | | -| | per request in | | -| | benchmark | | -+-----------------------+-----------------------+-----------------------+ -| Input Sequence Length | Total number of input | Avg, min, max, p99, | -| | tokens of a request, | p90, p75 | -| | one value per request | | -| | in benchmark | | -+-----------------------+-----------------------+-----------------------+ -| Output Token | Total number of | None–one value per | -| Throughput | output tokens from | benchmark | -| | benchmark divided by | | -| | benchmark duration | | -+-----------------------+-----------------------+-----------------------+ -| Request Throughput | Number of final | None–one value per | -| | responses from | benchmark | -| | benchmark divided by | | -| | benchmark duration | | -+-----------------------+-----------------------+-----------------------+ - -.. raw:: html - - - -Command Line Options --------------------- - -``-h`` -'''''' - -``--help`` -'''''''''' - -Show the help message and exit. - -Endpoint Options: -~~~~~~~~~~~~~~~~~ - -``-m `` -''''''''''''' - -``--model `` -'''''''''''''''''' - -The names of the models to benchmark. A single model is recommended, -unless you are `profiling multiple LoRA adapters `__. -(default: ``None``) - -``--model-selection-strategy {round_robin, random}`` -'''''''''''''''''''''''''''''''''''''''''''''''''''' - -When multiple models are specified, this is how a specific model is -assigned to a prompt. Round robin means that each model receives a -request in order. Random means that assignment is uniformly random -(default: ``round_robin``) - -``--backend {tensorrtllm,vllm}`` -'''''''''''''''''''''''''''''''' - -When using the “triton” service-kind, this is the backend of the model. -For the TRT-LLM backend, you currently must set -``exclude_input_in_output`` to true in the model config to not echo the -input tokens in the output. (default: tensorrtllm) - -``--endpoint `` -'''''''''''''''''''' - -Set a custom endpoint that differs from the OpenAI defaults. (default: -``None``) - -``--endpoint-type {chat,completions,embeddings,rankings}`` -'''''''''''''''''''''''''''''''''''''''''''''''''''''''''' - -The endpoint-type to send requests to on the server. This is only used -with the ``openai`` service-kind. (default: ``None``) - -``--service-kind {triton,openai}`` -'''''''''''''''''''''''''''''''''' - -The kind of service perf_analyzer will generate load for. In order to -use ``openai``, you must specify an api via ``--endpoint-type``. -(default: ``triton``) - -``--streaming`` -''''''''''''''' - -An option to enable the use of the streaming API. (default: ``False``) - -``-u `` -'''''''''''' - -``--url `` -''''''''''''''' - -URL of the endpoint to target for benchmarking. (default: ``None``) - -Input Options -~~~~~~~~~~~~~ - -``-b `` -'''''''''''' - -``--batch-size `` -'''''''''''''''''''''' - -The batch size of the requests GenAI-Perf should send. This is currently -only supported with the `embeddings `__, -image_retrieval, and `rankings `__ endpoint types. -(default: ``1``) - -``--extra-inputs `` -'''''''''''''''''''''''' - -Provide additional inputs to include with every request. You can repeat -this flag for multiple inputs. Inputs should be in an input_name:value -format. Alternatively, a string representing a json formatted dict can -be provided. (default: ``None``) - -``--input-dataset {openorca,cnn_dailymail}`` -'''''''''''''''''''''''''''''''''''''''''''' - -The HuggingFace dataset to use for prompts. (default: ``openorca``) - -``--input-file `` -''''''''''''''''''''''' - -The input file containing the prompts to use for profiling. Each line -should be a JSON object with a ‘text_input’ field in JSONL format. -Example: {"text_input": "Your prompt here"}" - -``--num-prompts `` -''''''''''''''''''''''' - -The number of unique prompts to generate as stimulus. (default: ``100``) - -``--output-tokens-mean `` -'''''''''''''''''''''''''''''' - -The mean number of tokens in each output. Ensure the ``--tokenizer`` -value is set correctly. (default: ``-1``) - -``--output-tokens-mean-deterministic`` -'''''''''''''''''''''''''''''''''''''' - -When using ``--output-tokens-mean``, this flag can be set to improve -precision by setting the minimum number of tokens equal to the requested -number of tokens. This is currently supported with the Triton -service-kind. Note that there is still some variability in the requested -number of output tokens, but GenAi-Perf attempts its best effort with -your model to get the right number of output tokens. (default: -``False``) - -``--output-tokens-stddev `` -'''''''''''''''''''''''''''''''' - -The standard deviation of the number of tokens in each output. This is -only used when ``--output-tokens-mean`` is provided. (default: ``0``) - -``--random-seed `` -''''''''''''''''''''''' - -The seed used to generate random values. (default: ``0``) - -``--synthetic-input-tokens-mean `` -''''''''''''''''''''''''''''''''''''''' - -The mean of number of tokens in the generated prompts when using -synthetic data. (default: ``550``) - -``--synthetic-input-tokens-stddev `` -''''''''''''''''''''''''''''''''''''''''' - -The standard deviation of number of tokens in the generated prompts when -using synthetic data. (default: ``0``) - -Profiling Options -~~~~~~~~~~~~~~~~~ - -``--concurrency `` -''''''''''''''''''''''' - -The concurrency value to benchmark. (default: ``None``) - -``--measurement-interval `` -'''''''''''''''''''''''''''''''' - -``-p `` -'''''''''''' - -The time interval used for each measurement in milliseconds. Perf -Analyzer will sample a time interval specified and take measurement over -the requests completed within that time interval. (default: ``10000``) - -``--request-rate `` -'''''''''''''''''''''''''' - -Sets the request rate for the load generated by PA. (default: ``None``) - -``-s `` -'''''''''''''' - -``--stability-percentage `` -'''''''''''''''''''''''''''''''''' - -The allowed variation in latency measurements when determining if a -result is stable. The measurement is considered as stable if the ratio -of max / min from the recent 3 measurements is within (stability -percentage) in terms of both infer per second and latency. (default: -``999``) - -Output Options -~~~~~~~~~~~~~~ - -``--artifact-dir`` -'''''''''''''''''' - -The directory to store all the (output) artifacts generated by -GenAI-Perf and Perf Analyzer. (default: ``artifacts``) - -``--generate-plots`` -'''''''''''''''''''' - -An option to enable the generation of plots. (default: False) - -``--profile-export-file `` -'''''''''''''''''''''''''''''''' - -The path where the perf_analyzer profile export will be generated. By -default, the profile export will be to ``profile_export.json``. The -genai-perf files will be exported to -``_genai_perf.json`` and -``_genai_perf.csv``. For example, if the profile -export file is ``profile_export.json``, the genai-perf file will be -exported to ``profile_export_genai_perf.csv``. (default: -``profile_export.json``) - -Other Options -~~~~~~~~~~~~~ - -``--tokenizer `` -''''''''''''''''''''' - -The HuggingFace tokenizer to use to interpret token metrics from prompts -and responses. (default: ``hf-internal-testing/llama-tokenizer``) - -``-v`` -'''''' - -``--verbose`` -''''''''''''' - -An option to enable verbose mode. (default: ``False``) - -``--version`` -''''''''''''' - -An option to print the version and exit. - -.. raw:: html - - - -Known Issues ------------- - -- GenAI-Perf can be slow to finish if a high request-rate is provided -- Token counts may not be exact diff --git a/docs/perf_benchmark/genai_perf.rst b/docs/perf_benchmark/genai_perf.rst index 175662477f..65de2d8ce1 100644 --- a/docs/perf_benchmark/genai_perf.rst +++ b/docs/perf_benchmark/genai_perf.rst @@ -1,5 +1,5 @@ .. -.. Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. .. Redistribution and use in source and binary forms, with or without .. modification, are permitted provided that the following conditions @@ -28,13 +28,12 @@ #### GenAI Performance Analyzer #### -.. include:: genai-perf-README.rst - .. toctree:: :maxdepth: 1 :hidden: + Overview <../perf_analyzer/genai-perf/README.md> Large language models <../perf_analyzer/genai-perf/docs/tutorial.md> Visual language models <../perf_analyzer/genai-perf/docs/multi_modal.md> Embedding models <../perf_analyzer/genai-perf/docs/embeddings.md> diff --git a/docs/perf_benchmark/model-analyzer-README.rst b/docs/perf_benchmark/model-analyzer-README.rst deleted file mode 100644 index f31e7ca633..0000000000 --- a/docs/perf_benchmark/model-analyzer-README.rst +++ /dev/null @@ -1,203 +0,0 @@ -.. -.. Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -.. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions -.. are met: -.. * Redistributions of source code must retain the above copyright -.. notice, this list of conditions and the following disclaimer. -.. * Redistributions in binary form must reproduce the above copyright -.. notice, this list of conditions and the following disclaimer in the -.. documentation and/or other materials provided with the distribution. -.. * Neither the name of NVIDIA CORPORATION nor the names of its -.. contributors may be used to endorse or promote products derived -.. from this software without specific prior written permission. -.. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -.. EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -.. PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -.. CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -.. EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -.. PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -.. PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -.. OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -.. (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -.. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -.. raw:: html - - -|License| - -Triton Model Analyzer -===================== - - [!Warning] - - .. rubric:: LATEST RELEASE - :name: latest-release - - You are currently on the ``main`` branch which tracks - under-development progress towards the next release. The latest - release of the Triton Model Analyzer is 1.42.0 and is available on - branch - `r24.07 `__. - -Triton Model Analyzer is a CLI tool which can help you find a more -optimal configuration, on a given piece of hardware, for single, -multiple, ensemble, or BLS models running on a `Triton Inference -Server `__. Model -Analyzer will also generate reports to help you better understand the -trade-offs of the different configurations along with their compute and -memory requirements. - -Features -======== - -Search Modes -~~~~~~~~~~~~ - -- `Optuna Search `__ **-ALPHA - RELEASE-** allows you to search for every parameter that can be - specified in the model configuration, using a hyperparameter - optimization framework. Please see the - `Optuna `__ website if you are interested in - specific details on how the algorithm functions. - -- `Quick Search `__ will - **sparsely** search the `Max Batch - Size `__, - `Dynamic - Batching `__, - and `Instance - Group `__ - spaces by utilizing a heuristic hill-climbing algorithm to help you - quickly find a more optimal configuration - -- `Automatic Brute - Search `__ will - **exhaustively** search the `Max Batch - Size `__, - `Dynamic - Batching `__, - and `Instance - Group `__ - parameters of your model configuration - -- `Manual Brute Search `__ - allows you to create manual sweeps for every parameter that can be - specified in the model configuration - -Model Types -~~~~~~~~~~~ - -- `Ensemble `__: Model Analyzer can help - you find the optimal settings when profiling an ensemble model - -- `BLS `__: Model Analyzer can help you find - the optimal settings when profiling a BLS model - -- `Multi-Model `__: Model Analyzer can - help you find the optimal settings when profiling multiple concurrent - models - -- `LLM `__: Model Analyzer can help you find - the optimal settings when profiling Large Language Models - -Other Features -~~~~~~~~~~~~~~ - -- `Detailed and summary reports `__: Model Analyzer is - able to generate summarized and detailed reports that can help you - better understand the trade-offs between different model - configurations that can be used for your model. - -- `QoS Constraints `__: Constraints can help - you filter out the Model Analyzer results based on your QoS - requirements. For example, you can specify a latency budget to filter - out model configurations that do not satisfy the specified latency - threshold. - -Examples and Tutorials -====================== - -**Single Model** -~~~~~~~~~~~~~~~~ - -See the `Single Model Quick Start `__ for a guide -on how to use Model Analyzer to profile, analyze and report on a simple -PyTorch model. - -**Multi Model** -~~~~~~~~~~~~~~~ - -See the `Multi-model Quick Start `__ for a guide -on how to use Model Analyzer to profile, analyze and report on two -models running concurrently on the same GPU. - -**Ensemble Model** -~~~~~~~~~~~~~~~~~~ - -See the `Ensemble Model Quick Start `__ -for a guide on how to use Model Analyzer to profile, analyze and report -on a simple Ensemble model. - -**BLS Model** -~~~~~~~~~~~~~ - -See the `BLS Model Quick Start `__ for a guide -on how to use Model Analyzer to profile, analyze and report on a simple -BLS model. - -Documentation -============= - -- `Installation `__ -- `Model Analyzer CLI `__ -- `Launch Modes `__ -- `Configuring Model Analyzer `__ -- `Model Analyzer Metrics `__ -- `Model Config Search `__ -- `Model Types `__ -- `Checkpointing `__ -- `Model Analyzer Reports `__ -- `Deployment with Kubernetes `__ - -Terminology -=========== - -Below are definitions of some commonly used terms in Model Analyzer: - -- **Model Type** - Category of model being profiled. Examples of this - include single, multi, ensemble, BLS, etc.. -- **Search Mode** - How Model Analyzer explores the possible - configuration space when profiling. This is either exhaustive (brute) - or heuristic (quick/optuna). -- **Model Config Search** - The cross product of model type and search - mode. -- **Launch Mode** - How the Triton Server is deployed and used by Model - Analyzer. - -Reporting problems, asking questions -==================================== - -We appreciate any feedback, questions or bug reporting regarding this -project. When help with code is needed, follow the process outlined in -the Stack Overflow (https://stackoverflow.com/help/mcve) document. -Ensure posted examples are: - -- minimal – use as little code as possible that still produces the same - problem - -- complete – provide all parts needed to reproduce the problem. Check - if you can strip external dependency and still show the problem. The - less time we spend on reproducing problems the more time we have to - fix it - -- verifiable – test the code you’re about to provide to make sure it - reproduces the problem. Remove all other problems that are not - related to your request/question. - -.. |License| image:: https://img.shields.io/badge/License-Apache_2.0-lightgrey.svg - :target: https://opensource.org/licenses/Apache-2.0 diff --git a/docs/perf_benchmark/model_analyzer.rst b/docs/perf_benchmark/model_analyzer.rst index c29a96aa92..a3c734ce30 100644 --- a/docs/perf_benchmark/model_analyzer.rst +++ b/docs/perf_benchmark/model_analyzer.rst @@ -1,5 +1,5 @@ .. -.. Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. .. Redistribution and use in source and binary forms, with or without .. modification, are permitted provided that the following conditions @@ -29,17 +29,23 @@ Model Analyzer #### -.. include:: model-analyzer-README.rst - .. toctree:: :maxdepth: 1 :hidden: - ../model_analyzer/docs/cli.md - ../model_analyzer/docs/launch_modes.md - ../model_analyzer/docs/config.md - ../model_analyzer/docs/metrics.md - ../model_analyzer/docs/config_search.md - ../model_analyzer/docs/checkpoints.md - ../model_analyzer/docs/report.md - ../model_analyzer/docs/kubernetes_deploy.md \ No newline at end of file + Overview <../model_analyzer/README.md> + Documentation <../model_analyzer/docs/README.md> + Quick Start <../model_analyzer/docs/quick_start.md> + Installation <../model_analyzer/docs/install.md> + CLI Reference <../model_analyzer/docs/cli.md> + Launch Modes <../model_analyzer/docs/launch_modes.md> + Configuration <../model_analyzer/docs/config.md> + Configuration Search <../model_analyzer/docs/config_search.md> + Metrics <../model_analyzer/docs/metrics.md> + Checkpointing <../model_analyzer/docs/checkpoints.md> + Reports <../model_analyzer/docs/report.md> + Kubernetes <../model_analyzer/docs/kubernetes_deploy.md> + Model Types <../model_analyzer/docs/model_types.md> + Ensemble Model <../model_analyzer/docs/ensemble_quick_start.md> + BLS Model <../model_analyzer/docs/bls_quick_start.md> + Multi-Model <../model_analyzer/docs/mm_quick_start.md> \ No newline at end of file diff --git a/docs/perf_benchmark/perf-analyzer-README.rst b/docs/perf_benchmark/perf-analyzer-README.rst deleted file mode 100644 index df9c246eb3..0000000000 --- a/docs/perf_benchmark/perf-analyzer-README.rst +++ /dev/null @@ -1,175 +0,0 @@ -.. -.. Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -.. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions -.. are met: -.. * Redistributions of source code must retain the above copyright -.. notice, this list of conditions and the following disclaimer. -.. * Redistributions in binary form must reproduce the above copyright -.. notice, this list of conditions and the following disclaimer in the -.. documentation and/or other materials provided with the distribution. -.. * Neither the name of NVIDIA CORPORATION nor the names of its -.. contributors may be used to endorse or promote products derived -.. from this software without specific prior written permission. -.. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -.. EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -.. PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -.. CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -.. EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -.. PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -.. PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -.. OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -.. (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -.. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -.. raw:: html - - -Triton Performance Analyzer -=========================== - -Triton Performance Analyzer is CLI tool which can help you optimize the -inference performance of models running on Triton Inference Server by -measuring changes in performance as you experiment with different -optimization strategies. - -Features -======== - -Inference Load Modes -~~~~~~~~~~~~~~~~~~~~ - -- `Concurrency Mode `__ - simlulates load by maintaining a specific concurrency of outgoing - requests to the server - -- `Request Rate - Mode `__ simulates - load by sending consecutive requests at a specific rate to the server - -- `Custom Interval - Mode `__ simulates - load by sending consecutive requests at specific intervals to the - server - -Performance Measurement Modes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- `Time Windows Mode `__ - measures model performance repeatedly over a specific time interval - until performance has stabilized - -- `Count Windows Mode `__ - measures model performance repeatedly over a specific number of - requests until performance has stabilized - -Other Features -~~~~~~~~~~~~~~ - -- `Sequence Models <../user_guide/architecture.md#stateful-models>`__, - `Ensemble Models <../user_guide/architecture.md#ensemble-models>`__, - and `Decoupled Models <../user_guide/decoupled_models.md>`__ can be - profiled in addition to standard/stateless/coupled models - -- `Input Data `__ to model inferences can be - auto-generated or specified as well as verifying output - -Quick Start -=========== - -The steps below will guide you on how to start using Perf Analyzer. - -Step 1: Start Triton Container -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code:: bash - - export RELEASE= # e.g. to use the release from the end of February of 2023, do `export RELEASE=23.02` - - docker pull nvcr.io/nvidia/tritonserver:${RELEASE}-py3 - - docker run --gpus all --rm -it --net host nvcr.io/nvidia/tritonserver:${RELEASE}-py3 - -Step 2: Download ``simple`` Model -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code:: bash - - # inside triton container - git clone --depth 1 https://github.com/triton-inference-server/server - - mkdir model_repository ; cp -r server/docs/examples/model_repository/simple model_repository - -Step 3: Start Triton Server -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code:: bash - - # inside triton container - tritonserver --model-repository $(pwd)/model_repository &> server.log & - - # confirm server is ready, look for 'HTTP/1.1 200 OK' - curl -v localhost:8000/v2/health/ready - - # detach (CTRL-p CTRL-q) - -Step 4: Start Triton SDK Container -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code:: bash - - docker pull nvcr.io/nvidia/tritonserver:${RELEASE}-py3-sdk - - docker run --gpus all --rm -it --net host nvcr.io/nvidia/tritonserver:${RELEASE}-py3-sdk - -Step 5: Run Perf Analyzer -~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code:: bash - - # inside sdk container - perf_analyzer -m simple - -See the full `quick start guide `__ for additional -tips on how to analyze output. - -Documentation -============= - -- `Installation `__ -- `Perf Analyzer CLI `__ -- `Inference Load Modes `__ -- `Input Data `__ -- `Measurements & Metrics `__ -- `Benchmarking `__ - -Contributing -============ - -Contributions to Triton Perf Analyzer are more than welcome. To -contribute please review the `contribution -guidelines `__, -then fork and create a pull request. - -Reporting problems, asking questions -==================================== - -We appreciate any feedback, questions or bug reporting regarding this -project. When help with code is needed, follow the process outlined in -the Stack Overflow (https://stackoverflow.com/help/mcve) document. -Ensure posted examples are: - -- minimal - use as little code as possible that still produces the same - problem - -- complete - provide all parts needed to reproduce the problem. Check - if you can strip external dependency and still show the problem. The - less time we spend on reproducing problems the more time we have to - fix it - -- verifiable - test the code you’re about to provide to make sure it - reproduces the problem. Remove all other problems that are not - related to your request/question. diff --git a/docs/perf_benchmark/perf_analyzer.rst b/docs/perf_benchmark/perf_analyzer.rst index d6c6156a62..fcb25e0ca8 100644 --- a/docs/perf_benchmark/perf_analyzer.rst +++ b/docs/perf_benchmark/perf_analyzer.rst @@ -1,5 +1,5 @@ .. -.. Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. .. Redistribution and use in source and binary forms, with or without .. modification, are permitted provided that the following conditions @@ -29,14 +29,16 @@ Performance Analyzer #### -.. include:: perf-analyzer-README.rst - .. toctree:: :maxdepth: 1 :hidden: - ../perf_analyzer/docs/install.md - ../perf_analyzer/docs/CLI.md - ../perf_analyzer/docs/inference_load_modes.md - ../perf_analyzer/docs/input_data.md - ../perf_analyzer/docs/measurements_metrics.md \ No newline at end of file + Overview <../perf_analyzer/README.md> + Documentation <../perf_analyzer/docs/README.md> + Quick Start <../perf_analyzer/docs/quick_start.md> + Installation <../perf_analyzer/docs/install.md> + CLI Reference <../perf_analyzer/docs/cli.md> + Inference Load Modes <../perf_analyzer/docs/inference_load_modes.md> + Input Data <../perf_analyzer/docs/input_data.md> + Measurement Modes <../perf_analyzer/docs/measurements_metrics.md> + Benchmarking <../perf_analyzer/docs/benchmarking.md> \ No newline at end of file From 22fd79ea3271fe64e34746995986dc046ad02af1 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:27:04 -0800 Subject: [PATCH 156/230] Update variables for models generation. Addressing legacy variables values. (#8667) --- qa/common/gen_jetson_trt_models | 4 ++-- qa/common/gen_qa_model_repository | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qa/common/gen_jetson_trt_models b/qa/common/gen_jetson_trt_models index af4f648e1c..aa507807c3 100755 --- a/qa/common/gen_jetson_trt_models +++ b/qa/common/gen_jetson_trt_models @@ -36,13 +36,13 @@ umask 0000 # Set the version of the models TRITON_VERSION=${TRITON_VERSION:=26.01} # Set the CUDA device to use -CUDA_DEVICE=${RUNNER_ID:=0} +NVIDIA_VISIBLE_DEVICES=${NVIDIA_VISIBLE_DEVICES:=0} # Set TensorRT image TENSORRT_IMAGE=${TENSORRT_IMAGE:=nvcr.io/nvidia/tensorrt:$TRITON_VERSION-py3-igpu} UBUNTU_IMAGE=${UBUNTU_IMAGE:=ubuntu:24.04} # Set CI specific parameters -DOCKER_GPU_ARGS=${DOCKER_GPU_ARGS:-$([[ $RUNNER_GPUS =~ ^[0-9] ]] && eval $NV_DOCKER_ARGS || echo "--gpus device=$CUDA_DEVICE" )} +DOCKER_GPU_ARGS=${DOCKER_GPU_ARGS:-$([[ -v RUNNER_GPUS && $RUNNER_GPUS =~ ^[0-9] ]] && eval $NV_DOCKER_ARGS || echo "--runtime=nvidia -e NVIDIA_VISIBLE_DEVICES=$NVIDIA_VISIBLE_DEVICES" )} ############################################################################ # Check if Docker volume exists diff --git a/qa/common/gen_qa_model_repository b/qa/common/gen_qa_model_repository index 429d15d736..dc63c6daee 100755 --- a/qa/common/gen_qa_model_repository +++ b/qa/common/gen_qa_model_repository @@ -34,7 +34,7 @@ ## match what is being used by the tritonserver release being ## tested. ## -## 2. Set CUDA_DEVICE to the ID of the CUDA device present on the +## 2. Set NVIDIA_VISIBLE_DEVICES to the ID of the CUDA device present on the ## local system that you want to target for the generated models. ## ## 3. Run this script to create /tmp/qa_model_repository, @@ -67,7 +67,7 @@ OPENVINO_VERSION=2024.5.0 UBUNTU_IMAGE=${UBUNTU_IMAGE:=ubuntu:22.04} PYTORCH_IMAGE=${PYTORCH_IMAGE:=nvcr.io/nvidia/pytorch:$TRITON_VERSION-py3} TENSORRT_IMAGE=${TENSORRT_IMAGE:=nvcr.io/nvidia/tensorrt:$TRITON_VERSION-py3} -CUDA_DEVICE=${NV_GPU:=0} +NVIDIA_VISIBLE_DEVICES=${NVIDIA_VISIBLE_DEVICES:=0} MODEL_TYPE=${MODEL_TYPE:-""} TRITON_MODELS_USE_DOCKER=${TRITON_MODELS_USE_DOCKER:-"1"} TRITON_MODELS_USE_ENROOT=${TRITON_MODELS_USE_ENROOT:-"1"} @@ -80,7 +80,7 @@ PROJECT_NAME=${PROJECT_NAME:=tritonserver} log_message.status "define: Docker engine parameters" DOCKER_VOLUME=${DOCKER_VOLUME:=volume.gen_qa_model_repository.${CI_JOB_ID}} DOCKER_VOLUME_CONTAINER=${DOCKER_VOLUME}.gen_qa_model_repository.${CI_JOB_ID} -DOCKER_GPU_ARGS=${DOCKER_GPU_ARGS:-$([[ $RUNNER_GPUS =~ ^[0-9] ]] && eval $NV_DOCKER_ARGS || echo "--gpus device=$CUDA_DEVICE" )} +DOCKER_GPU_ARGS=${DOCKER_GPU_ARGS:-$([[ -v RUNNER_GPUS && $RUNNER_GPUS =~ ^[0-9] ]] && eval $NV_DOCKER_ARGS || echo "--runtime=nvidia -e NVIDIA_VISIBLE_DEVICES=$NVIDIA_VISIBLE_DEVICES" )} function define_model_output_directories() { log_message.status "define: model output directories" From a202ae54f85348a1a73c42b97f8502dad4db663a Mon Sep 17 00:00:00 2001 From: J Wyman Date: Wed, 25 Feb 2026 11:23:30 -0500 Subject: [PATCH 157/230] fix: Path Traversal Bug in SageMaker Server (#8659) Co-authored-by: Yingge He <157551214+yinggeh@users.noreply.github.com> --- src/sagemaker_server.cc | 51 ++++++++++++++++++++++++++++++++++------- src/sagemaker_server.h | 3 ++- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/sagemaker_server.cc b/src/sagemaker_server.cc index cf379e910e..637be6fc9c 100644 --- a/src/sagemaker_server.cc +++ b/src/sagemaker_server.cc @@ -23,8 +23,11 @@ // OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + #include "sagemaker_server.h" +#include + namespace triton { namespace server { #define HTTP_RESPOND_IF_ERR(REQ, X) \ @@ -291,6 +294,22 @@ SagemakerAPIServer::ParseSageMakerRequest( } } + std::filesystem::path url_path(url_string); + url_path = std::filesystem::absolute( + url_path.lexically_normal()); // Normalize the path to remove any + // redundant components. + auto url_abspath = url_path.string(); + + if (url_abspath.find("/dev/") == 0 || url_abspath.find("/proc/") == 0 || + url_abspath.find("/sys/") == 0) { + LOG_ERROR << "Invalid URL: " << url_string + << ". \"url\" property value cannot start with /dev/, /proc/, or " + "/sys/." + << std::endl; + evhtp_send_reply(req, EVHTP_RES_BADREQ); + return; + } + if (action == "load") { (*parse_map)["url"] = url_string.c_str(); } @@ -891,11 +910,27 @@ SagemakerAPIServer::SageMakerMMELoadModel( evhtp_request_t* req, const std::unordered_map parse_map) { - std::string repo_path = parse_map.at("url"); + std::string url_string = parse_map.at("url"); std::string model_name_hash = parse_map.at("model_name_hash"); std::string target_model = parse_map.at("target_model"); - /* Check subdirs for models and find ensemble model within the repo_path + std::filesystem::path url_path(url_string); + url_path = std::filesystem::absolute( + url_path.lexically_normal()); // Normalize the path to remove any + // redundant components. + std::string url_abspath = url_path.string(); + + if (url_abspath.find("/dev/") == 0 || url_abspath.find("/proc/") == 0 || + url_abspath.find("/sys/") == 0) { + LOG_ERROR << "Invalid repository path: " << url_string + << ". \"url\" property of `parse_map`cannot start with /dev/, " + "/proc/, or /sys/." + << std::endl; + evhtp_send_reply(req, EVHTP_RES_BADREQ); + return; + } + + /* Check subdirs for models and find ensemble model within the url_abspath * If only 1 model, that will be selected as model_subdir * Else ensemble model directory is set as model_subdir */ @@ -904,7 +939,8 @@ SagemakerAPIServer::SageMakerMMELoadModel( int dir_count = 0; std::string model_subdir, ensemble_model_subdir; - if ((dir = opendir(repo_path.c_str())) != NULL) { + if ((dir = opendir(url_abspath.c_str())) != NULL) { + std::shared_ptr
dir_ptr{dir, closedir}; while ((ent = readdir(dir)) != NULL) { if ((ent->d_type == DT_DIR) && (!strcmp(ent->d_name, ".") == 0) && (!strcmp(ent->d_name, "..") == 0)) { @@ -923,7 +959,7 @@ SagemakerAPIServer::SageMakerMMELoadModel( // Read the config.pbtxt file at each path, if available std::string ensemble_config_path = - repo_path + "/" + model_subdir + "/" + "config.pbtxt"; + url_abspath + "/" + model_subdir + "/config.pbtxt"; std::ifstream config_fstream(ensemble_config_path); std::stringstream ensemble_config_content; @@ -954,7 +990,6 @@ SagemakerAPIServer::SageMakerMMELoadModel( << std::endl; } } - closedir(dir); } if (!strcmp(ensemble_model_subdir.c_str(), "") == 0) { @@ -972,10 +1007,10 @@ SagemakerAPIServer::SageMakerMMELoadModel( std::string repo_parent_path, subdir, customer_subdir; RE2::FullMatch( - repo_path, model_path_regex_, &repo_parent_path, &subdir, + url_abspath, model_path_regex_, &repo_parent_path, &subdir, &customer_subdir); - std::string config_path = repo_path + "/config.pbtxt"; + std::string config_path = url_abspath + "/config.pbtxt"; struct stat buffer; /* If config.pbtxt is at repo root, @@ -986,7 +1021,7 @@ SagemakerAPIServer::SageMakerMMELoadModel( if (stat(config_path.c_str(), &buffer) == 0) { model_subdir = subdir; } else { - repo_parent_path = repo_path; + repo_parent_path = url_abspath; } auto param = TRITONSERVER_ParameterNew( diff --git a/src/sagemaker_server.h b/src/sagemaker_server.h index dcd40e66ac..1aef149605 100644 --- a/src/sagemaker_server.h +++ b/src/sagemaker_server.h @@ -1,4 +1,4 @@ -// Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -23,6 +23,7 @@ // OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + #pragma once #include From bdd1c3c88102cd7c72ccb3f50bffe4b217989fb3 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:19:36 -0800 Subject: [PATCH 158/230] test: Correct misuse of log file argument (#8673) --- qa/common/check_valgrind_log.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qa/common/check_valgrind_log.py b/qa/common/check_valgrind_log.py index 84eecd4989..8a59dd3e9c 100755 --- a/qa/common/check_valgrind_log.py +++ b/qa/common/check_valgrind_log.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -58,7 +58,7 @@ def check_valgrind_log(log_file): a list of the leak records as strings """ - with open(args.input_log_file, "r") as f: + with open(log_file, "r") as f: logs = f.read() # Find the pid and start and end of definite leak reports From df3dec3430ae9d2f9f12883468f0a1290305ef05 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:04:00 -0800 Subject: [PATCH 159/230] fix: Additional validations for OpenAI frontend LoRA paths (#8669) --- .../openai_frontend/engine/utils/triton.py | 45 +++++++---- python/openai/tests/test_lora.py | 75 +++++++++++++++++-- 2 files changed, 97 insertions(+), 23 deletions(-) diff --git a/python/openai/openai_frontend/engine/utils/triton.py b/python/openai/openai_frontend/engine/utils/triton.py index 8dd7663f3a..91925e16b0 100644 --- a/python/openai/openai_frontend/engine/utils/triton.py +++ b/python/openai/openai_frontend/engine/utils/triton.py @@ -617,6 +617,34 @@ def _get_guided_json_from_tool( return None +def _validate_lora_path_trtllm(repo_path: str, lora_path: str, lora_name: str): + if os.path.isabs(lora_path): + raise ValueError( + f"LoRA path '{lora_path}' for '{lora_name}' must be a relative path inside its model repository" + ) + + # NOTE: Error messages should never contain the real/absolute paths. + realpath_repo = os.path.realpath(repo_path) + realpath_lora = os.path.realpath(os.path.join(realpath_repo, lora_path)) + # Always check if the LoRA path is inside the model repository before checking its existence. + if os.path.commonpath([realpath_repo, realpath_lora]) != realpath_repo: + raise ValueError( + f"LoRA path '{lora_path}' for '{lora_name}' must be inside its model repository" + ) + if not os.path.exists(realpath_lora): + raise ServerError( + f"LoRA directory '{lora_path}' not found for '{lora_name}' in its model repository" + ) + + # Check if the files exist + for lora_file in ["model.lora_weights.npy", "model.lora_config.npy"]: + lora_file_path = os.path.join(realpath_lora, lora_file) + if not os.path.exists(lora_file_path): + raise ServerError( + f"LoRA file '{lora_file}' not found for '{lora_name}' at path: {lora_file_path}" + ) + + def _parse_lora_configs( model_repository: str | list[str], model_name: str, model_version: int, backend: str ) -> None | List[tuple[str, str]]: @@ -683,25 +711,10 @@ def _parse_lora_configs( with open(lora_config_path, "r") as f: lora_config = json.load(f) for lora_name, lora_path in lora_config.items(): - print(f"backend: {backend}") if backend == "vllm": lora_configs.append(TritonLoraConfig(name=lora_name)) else: - lora_weights_path = os.path.join( - lora_path, "model.lora_weights.npy" - ) - lora_config_path = os.path.join( - lora_path, "model.lora_config.npy" - ) - if not os.path.exists(lora_weights_path): - raise ServerError( - f"LoRA weights file not found for '{lora_name}' at path: {lora_weights_path}" - ) - if not os.path.exists(lora_config_path): - raise ServerError( - f"LoRA config file not found for '{lora_name}' at path: {lora_config_path}" - ) - + _validate_lora_path_trtllm(repo_path, lora_path, lora_name) lora_configs.append( TritonLoraConfig( name=lora_name, path=lora_path, task_id=lora_task_id diff --git a/python/openai/tests/test_lora.py b/python/openai/tests/test_lora.py index a073f4b93e..abcc956f32 100644 --- a/python/openai/tests/test_lora.py +++ b/python/openai/tests/test_lora.py @@ -1,4 +1,4 @@ -# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -35,10 +35,22 @@ from openai_frontend.engine.utils.triton import ( _parse_lora_configs as parse_lora_configs, ) +from openai_frontend.engine.utils.triton import ( + _validate_lora_path_trtllm as validate_lora_path_trtllm, +) from .utils import OpenAIServer +def is_vllm_installed(): + try: + import vllm as _ + + return True + except ImportError: + return False + + @pytest.mark.parametrize( "model_repository,model_name,expect_error", [ @@ -74,13 +86,62 @@ def test_parse_lora_configs(model_repository: str, model_name: str, expect_error ) -def is_vllm_installed(): +@pytest.mark.skipif( + is_vllm_installed(), + reason="VLLM backend does not validate LoRA paths", +) +@pytest.mark.parametrize( + "lora_path,expect_error,error_message", + [ + # Valid relative path inside repo (requires .npy files to exist at runtime). + ("tensorrt_llm_bls/1/luotuo-lora-7b-0.1-weights", False, None), + ("tensorrt_llm_bls/1/Japanese-Alpaca-LoRA-7b-v0-weights", False, None), + # Absolute path not allowed. + ( + os.path.join( + os.path.abspath(os.curdir), + "tests/tensorrtllm_models", + "tensorrt_llm_bls/1/luotuo-lora-7b-0.1-weights", + ), + True, + f"must be a relative path inside its model repository", + ), + ("/etc/passwd", True, "must be a relative path inside its model repository"), + # Path outside repo (traversal). + ("tensorrt_llm_bls/1//../1/luotuo-lora-7b-0.1-weights", False, None), + ("../outside/lora", True, "must be inside its model repository"), + ("subdir/../../etc/passwd", True, "must be inside its model repository"), + # LoRA directory not found. + ("tensorrt_llm_bls/10", True, "LoRA directory 'tensorrt_llm_bls/10' not found"), + ( + "tensorrt_llm_bls/1/non_exist", + True, + "LoRA directory 'tensorrt_llm_bls/1/non_exist' not found", + ), + # LoRA file not found. + ("tensorrt_llm_bls/1", True, "LoRA file 'model.lora_weights.npy' not found"), + ], +) +def test_validate_lora_path_trtllm( + lora_path: str, + expect_error: bool, + error_message: str, +): + lora_name = "" + repo_path = "tests/tensorrtllm_models" try: - import vllm as _ - - return True - except ImportError: - return False + validate_lora_path_trtllm(repo_path, lora_path, lora_name) + except Exception as e: + if not expect_error: + raise pytest.fail( + f"repo_path='{repo_path}' raised exception unexpectedly: {e}" + ) + assert error_message in str(e) + else: + if expect_error: + raise pytest.fail( + f"lora_path='{repo_path}' did not raise exception as expected." + ) class LoRATest(unittest.TestCase): From 36f4b172d5a18a18f7432f33ac53c1da13eee057 Mon Sep 17 00:00:00 2001 From: Sai Kiran Polisetty Date: Tue, 3 Mar 2026 10:44:45 +0530 Subject: [PATCH 160/230] ci: Add model name validation for model management requests (#8672) --- .../input_validation_test.py | 73 ++++++++++++++++++- qa/L0_input_validation/test.sh | 28 ++++++- 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/qa/L0_input_validation/input_validation_test.py b/qa/L0_input_validation/input_validation_test.py index e3524247aa..5b5788c409 100755 --- a/qa/L0_input_validation/input_validation_test.py +++ b/qa/L0_input_validation/input_validation_test.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -288,5 +288,76 @@ def inference_helper(model_name, batch_size=1): inference_helper(model_name="plan_zero_1_float32_int32", batch_size=8) +class ModelNameValidationTest(unittest.TestCase): + INVALID_TRAVERSAL_NAMES = [ + "../etc", + "a/../b", + "../../etc/passwd", + "../../../../etc", + "model/..", + "..", + "/etc/passwd", + "model/subdir", + "model/", + " ..", + ".. ", + ] + + def test_model_name_invalid_load(self): + client = tritongrpcclient.InferenceServerClient("localhost:8001") + for model_name in self.INVALID_TRAVERSAL_NAMES: + with self.assertRaises(InferenceServerException) as cm: + client.load_model(model_name) + self.assertIn( + "model name must not contain path traversal characters", + str(cm.exception), + f"Expected traversal rejection for model name: {model_name!r}", + ) + + def test_model_name_empty_load(self): + client = tritongrpcclient.InferenceServerClient("localhost:8001") + with self.assertRaises(InferenceServerException) as cm: + client.load_model("") + self.assertIn("model name must not be empty", str(cm.exception)) + + def test_model_name_whitespace_only_load(self): + client = tritongrpcclient.InferenceServerClient("localhost:8001") + whitespace_names = [" ", " ", "\t", "\n", "\r", "\f", "\v", " \t \n "] + for model_name in whitespace_names: + with self.assertRaises(InferenceServerException) as cm: + client.load_model(model_name) + self.assertIn( + "model name must not contain only whitespace", + str(cm.exception), + f"Expected whitespace-only rejection for model name: {model_name!r}", + ) + + def test_model_name_invalid_unload(self): + # Unload should not trigger traversal check + client = tritongrpcclient.InferenceServerClient("localhost:8001") + for model_name in self.INVALID_TRAVERSAL_NAMES: + try: + client.unload_model(model_name) + except InferenceServerException as e: + self.assertNotIn( + "model name must not contain path traversal characters", + str(e), + f"Unload should not trigger traversal rejection for model name: {model_name!r}", + ) + + def test_model_name_valid(self): + """Verify that a syntactically valid model name is not rejected by + the traversal check -- it should fail with a model not found error instead.""" + client = tritongrpcclient.InferenceServerClient("localhost:8001") + with self.assertRaises(InferenceServerException) as cm: + client.load_model("nonexistent_model") + self.assertNotIn( + "path traversal characters", + str(cm.exception), + "Valid model name should not trigger path traversal rejection", + ) + self.assertIn("failed to poll from model repository", str(cm.exception)) + + if __name__ == "__main__": unittest.main() diff --git a/qa/L0_input_validation/test.sh b/qa/L0_input_validation/test.sh index 699fa97b02..da6e3735b9 100755 --- a/qa/L0_input_validation/test.sh +++ b/qa/L0_input_validation/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -175,6 +175,32 @@ if [ $? -ne 0 ]; then fi set -e +# Model name validation test +rm -rf test_models ; mkdir -p test_models +SERVER_LOG="./model_name_validation_server.log" +CLIENT_LOG="./model_name_validation_client.log" +SERVER_ARGS="--model-repository=`pwd`/test_models --model-control-mode=explicit --log-verbose=1" +run_server +if [ "$SERVER_PID" == "0" ]; then + echo -e "\n***\n*** Failed to start $SERVER\n***" + cat $SERVER_LOG + exit 1 +fi + +set +e +python3 -m pytest -s --junitxml="model_name_validation.report.xml" $TEST_PY::ModelNameValidationTest >> $CLIENT_LOG 2>&1 + +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + cat $SERVER_LOG + echo -e "\n***\n*** input_validation_test.py::ModelNameValidationTest FAILED. \n***" + RET=1 +fi +set -e + +kill $SERVER_PID +wait $SERVER_PID + if [ $RET -eq 0 ]; then echo -e "\n***\n*** Input Validation Test Passed\n***" else From 0a98c75d96668e4f8dd798c8f28e80be83e614b1 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Tue, 3 Mar 2026 08:40:54 -0800 Subject: [PATCH 161/230] post: Update default branch post-26.02 (#8681) --- Dockerfile.sdk | 2 +- README.md | 10 +++++----- build.py | 4 ++-- deploy/aws/values.yaml | 2 +- deploy/fleetcommand/Chart.yaml | 2 +- deploy/fleetcommand/values.yaml | 6 +++--- deploy/gcp/values.yaml | 2 +- .../benchmark/perf-analyzer-script/triton_client.yaml | 2 +- .../server-deployer/build_and_push.sh | 6 +++--- .../server-deployer/chart/triton/Chart.yaml | 2 +- .../server-deployer/chart/triton/values.yaml | 6 +++--- .../server-deployer/data-test/schema.yaml | 2 +- deploy/gke-marketplace-app/server-deployer/schema.yaml | 4 ++-- deploy/gke-marketplace-app/trt-engine/README.md | 6 +++--- deploy/k8s-onprem/values.yaml | 2 +- deploy/oci/values.yaml | 2 +- docs/introduction/compatibility.md | 3 +++ docs/introduction/release_notes.md | 4 ++-- python/openai/README.md | 6 +++--- qa/common/gen_jetson_trt_models | 2 +- qa/common/gen_qa_model_repository | 2 +- 21 files changed, 40 insertions(+), 37 deletions(-) diff --git a/Dockerfile.sdk b/Dockerfile.sdk index 36de500eed..f06dcb2501 100644 --- a/Dockerfile.sdk +++ b/Dockerfile.sdk @@ -29,7 +29,7 @@ # # Base image on the minimum Triton container -ARG BASE_IMAGE=nvcr.io/nvidia/tritonserver:26.01-py3-min +ARG BASE_IMAGE=nvcr.io/nvidia/tritonserver:26.02-py3-min ARG TRITON_CLIENT_REPO_SUBDIR=clientrepo ARG TRITON_REPO_ORGANIZATION=http://github.com/triton-inference-server diff --git a/README.md b/README.md index 37f7a38251..0ecbc03789 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ >[!WARNING] >You are currently on the `main` branch which tracks under-development progress ->towards the next release. The current release is version [2.65.0](https://github.com/triton-inference-server/server/releases/latest) ->and corresponds to the 26.01 container release on NVIDIA GPU Cloud (NGC). +>towards the next release. The current release is version [2.66.0](https://github.com/triton-inference-server/server/releases/latest) +>and corresponds to the 26.02 container release on NVIDIA GPU Cloud (NGC). # Triton Inference Server @@ -90,16 +90,16 @@ Inference Server with the ```bash # Step 1: Create the example model repository -git clone -b r26.01 https://github.com/triton-inference-server/server.git +git clone -b r26.02 https://github.com/triton-inference-server/server.git cd server/docs/examples ./fetch_models.sh # Step 2: Launch triton from the NGC Triton container -docker run --gpus=1 --rm --net=host -v ${PWD}/model_repository:/models nvcr.io/nvidia/tritonserver:26.01-py3 tritonserver --model-repository=/models --model-control-mode explicit --load-model densenet_onnx +docker run --gpus=1 --rm --net=host -v ${PWD}/model_repository:/models nvcr.io/nvidia/tritonserver:26.02-py3 tritonserver --model-repository=/models --model-control-mode explicit --load-model densenet_onnx # Step 3: Sending an Inference Request # In a separate console, launch the image_client example from the NGC Triton SDK container -docker run -it --rm --net=host nvcr.io/nvidia/tritonserver:26.01-py3-sdk /workspace/install/bin/image_client -m densenet_onnx -c 3 -s INCEPTION /workspace/images/mug.jpg +docker run -it --rm --net=host nvcr.io/nvidia/tritonserver:26.02-py3-sdk /workspace/install/bin/image_client -m densenet_onnx -c 3 -s INCEPTION /workspace/images/mug.jpg # Inference should return the following Image '/workspace/images/mug.jpg': diff --git a/build.py b/build.py index 70392b76f5..57acf198f6 100755 --- a/build.py +++ b/build.py @@ -73,12 +73,12 @@ DEFAULT_TRITON_VERSION_MAP = { "release_version": "2.67.0dev", "triton_container_version": "26.03dev", - "upstream_container_version": "26.01", + "upstream_container_version": "26.02", "ort_version": "1.24.1", "ort_openvino_version": "2025.4.1", "standalone_openvino_version": "2025.4.1", "dcgm_version": "4.5.2-1", - "vllm_version": "0.13.0", + "vllm_version": "0.15.1", "rhel_py_version": "3.12.3", } diff --git a/deploy/aws/values.yaml b/deploy/aws/values.yaml index 45cda89942..cea66962df 100644 --- a/deploy/aws/values.yaml +++ b/deploy/aws/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:26.01-py3 + imageName: nvcr.io/nvidia/tritonserver:26.02-py3 pullPolicy: IfNotPresent modelRepositoryPath: s3://triton-inference-server-repository/model_repository numGpus: 1 diff --git a/deploy/fleetcommand/Chart.yaml b/deploy/fleetcommand/Chart.yaml index 8760515775..d098482d50 100644 --- a/deploy/fleetcommand/Chart.yaml +++ b/deploy/fleetcommand/Chart.yaml @@ -26,7 +26,7 @@ apiVersion: v1 # appVersion is the Triton version; update when changing release -appVersion: 2.65.0 +appVersion: 2.66.0 description: Triton Inference Server (Fleet Command) name: triton-inference-server # version is the Chart version; update when changing anything in the chart diff --git a/deploy/fleetcommand/values.yaml b/deploy/fleetcommand/values.yaml index bc6fb0b759..f5563dc3a1 100644 --- a/deploy/fleetcommand/values.yaml +++ b/deploy/fleetcommand/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:26.01-py3 + imageName: nvcr.io/nvidia/tritonserver:26.02-py3 pullPolicy: IfNotPresent numGpus: 1 serverCommand: tritonserver @@ -47,13 +47,13 @@ image: # # To set model control mode, uncomment and configure below # TODO: Fix the following url, it is invalid - # See https://github.com/triton-inference-server/server/blob/r26.01/docs/user_guide/model_management.md + # See https://github.com/triton-inference-server/server/blob/r26.02/docs/user_guide/model_management.md # for more details #- --model-control-mode=explicit|poll|none # # Additional server args # - # see https://github.com/triton-inference-server/server/blob/r26.01/README.md + # see https://github.com/triton-inference-server/server/blob/r26.02/README.md # for more details service: diff --git a/deploy/gcp/values.yaml b/deploy/gcp/values.yaml index 4612dd4ae6..6ede77cf57 100644 --- a/deploy/gcp/values.yaml +++ b/deploy/gcp/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:26.01-py3 + imageName: nvcr.io/nvidia/tritonserver:26.02-py3 pullPolicy: IfNotPresent modelRepositoryPath: gs://triton-inference-server-repository/model_repository numGpus: 1 diff --git a/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml b/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml index 3d5abafa3b..149f72e63f 100644 --- a/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml +++ b/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml @@ -33,7 +33,7 @@ metadata: namespace: default spec: containers: - - image: nvcr.io/nvidia/tritonserver:26.01-py3-sdk + - image: nvcr.io/nvidia/tritonserver:26.02-py3-sdk imagePullPolicy: Always name: nv-triton-client securityContext: diff --git a/deploy/gke-marketplace-app/server-deployer/build_and_push.sh b/deploy/gke-marketplace-app/server-deployer/build_and_push.sh index 0121d266fb..a0ec4cb855 100755 --- a/deploy/gke-marketplace-app/server-deployer/build_and_push.sh +++ b/deploy/gke-marketplace-app/server-deployer/build_and_push.sh @@ -27,9 +27,9 @@ export REGISTRY=gcr.io/$(gcloud config get-value project | tr ':' '/') export APP_NAME=tritonserver -export MAJOR_VERSION=2.65 -export MINOR_VERSION=2.65.0 -export NGC_VERSION=26.01-py3 +export MAJOR_VERSION=2.66 +export MINOR_VERSION=2.66.0 +export NGC_VERSION=26.02-py3 docker pull nvcr.io/nvidia/$APP_NAME:$NGC_VERSION diff --git a/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml b/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml index ddbc69c63c..683c55e47a 100644 --- a/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml +++ b/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml @@ -28,4 +28,4 @@ apiVersion: v1 appVersion: "2.65" description: Triton Inference Server name: triton-inference-server -version: 2.65.0 +version: 2.66.0 diff --git a/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml b/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml index 15387d650d..f3e6454bba 100644 --- a/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml +++ b/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml @@ -31,14 +31,14 @@ maxReplicaCount: 3 tritonProtocol: HTTP # HPA GPU utilization autoscaling target HPATargetAverageValue: 85 -modelRepositoryPath: gs://triton_sample_models/26.01 -publishedVersion: '2.65.0' +modelRepositoryPath: gs://triton_sample_models/26.02 +publishedVersion: '2.66.0' gcpMarketplace: true image: registry: gcr.io repository: nvidia-ngc-public/tritonserver - tag: 26.01-py3 + tag: 26.02-py3 pullPolicy: IfNotPresent # modify the model repository here to match your GCP storage bucket numGpus: 1 diff --git a/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml b/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml index c63bcbc2d5..63ac8d7664 100644 --- a/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml +++ b/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml @@ -27,7 +27,7 @@ x-google-marketplace: schemaVersion: v2 applicationApiVersion: v1beta1 - publishedVersion: '2.65.0' + publishedVersion: '2.66.0' publishedVersionMetadata: releaseNote: >- Initial release. diff --git a/deploy/gke-marketplace-app/server-deployer/schema.yaml b/deploy/gke-marketplace-app/server-deployer/schema.yaml index f9c31d268a..2cdff30385 100644 --- a/deploy/gke-marketplace-app/server-deployer/schema.yaml +++ b/deploy/gke-marketplace-app/server-deployer/schema.yaml @@ -27,7 +27,7 @@ x-google-marketplace: schemaVersion: v2 applicationApiVersion: v1beta1 - publishedVersion: '2.65.0' + publishedVersion: '2.66.0' publishedVersionMetadata: releaseNote: >- Initial release. @@ -89,7 +89,7 @@ properties: modelRepositoryPath: type: string title: Bucket where models are stored. Please make sure the user/service account to create the GKE app has permission to this GCS bucket. Read Triton documentation on configs and formatting details, supporting TensorRT, TensorFlow, Pytorch, Onnx ... etc. - default: gs://triton_sample_models/26.01 + default: gs://triton_sample_models/26.02 image.ldPreloadPath: type: string title: Leave this empty by default. Triton allows users to create custom layers for backend such as TensorRT plugin, the compiled shared library must be provided via LD_PRELOAD environment variable. diff --git a/deploy/gke-marketplace-app/trt-engine/README.md b/deploy/gke-marketplace-app/trt-engine/README.md index 6f33ac0a1c..afbde3ff9f 100644 --- a/deploy/gke-marketplace-app/trt-engine/README.md +++ b/deploy/gke-marketplace-app/trt-engine/README.md @@ -33,7 +33,7 @@ ``` docker run --gpus all -it --network host \ --shm-size=1g --ulimit memlock=-1 --ulimit stack=67108864 \ - -v ~:/scripts nvcr.io/nvidia/tensorrt:26.01-py3 + -v ~:/scripts nvcr.io/nvidia/tensorrt:26.02-py3 pip install onnx six torch tf2onnx tensorflow @@ -57,7 +57,7 @@ mkdir -p engines python3 builder.py -m models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/model.ckpt -o engines/bert_large_int8_bs1_s128.engine -b 1 -s 128 -c models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/ -v models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/vocab.txt --int8 --fp16 --strict --calib-num 1 -iln -imh -gsutil cp bert_large_int8_bs1_s128.engine gs://triton_sample_models/26.01/bert/1/model.plan +gsutil cp bert_large_int8_bs1_s128.engine gs://triton_sample_models/26.02/bert/1/model.plan ``` -For each Triton upgrade, container version used to generate the model, and the model path in GCS `gs://triton_sample_models/26.01/` should be updated accordingly with the correct version. +For each Triton upgrade, container version used to generate the model, and the model path in GCS `gs://triton_sample_models/26.02/` should be updated accordingly with the correct version. diff --git a/deploy/k8s-onprem/values.yaml b/deploy/k8s-onprem/values.yaml index 347dc5ac42..405316696e 100644 --- a/deploy/k8s-onprem/values.yaml +++ b/deploy/k8s-onprem/values.yaml @@ -30,7 +30,7 @@ tags: openshift: false image: - imageName: nvcr.io/nvidia/tritonserver:26.01-py3 + imageName: nvcr.io/nvidia/tritonserver:26.02-py3 pullPolicy: IfNotPresent modelRepositoryServer: < Replace with the IP Address of your file server > modelRepositoryPath: /srv/models diff --git a/deploy/oci/values.yaml b/deploy/oci/values.yaml index 904c7a2d30..2ce8c749f9 100644 --- a/deploy/oci/values.yaml +++ b/deploy/oci/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:26.01-py3 + imageName: nvcr.io/nvidia/tritonserver:26.02-py3 pullPolicy: IfNotPresent modelRepositoryPath: s3://https://.compat.objectstorage..oraclecloud.com:443/triton-inference-server-repository numGpus: 1 diff --git a/docs/introduction/compatibility.md b/docs/introduction/compatibility.md index faa0039c80..3e6840bfde 100644 --- a/docs/introduction/compatibility.md +++ b/docs/introduction/compatibility.md @@ -38,6 +38,7 @@ | Triton release version | NGC Tag | Python version | Torch version | TensorRT version | TensorRT-LLM version | CUDA version | CUDA Driver version | Size | | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 26.02 | nvcr.io/nvidia/tritonserver:26.02-trtllm-python-py3 | Python 3.12.3 | 2.9.0a0+145a3a7bda.nv25.10 | 10.13.3.9 | 1.1.0 | 13.0.2.006 | 580.95.05 | 16.17 GB | | 26.01 | nvcr.io/nvidia/tritonserver:26.01-trtllm-python-py3 | Python 3.12.3 | 2.9.0a0+145a3a7bda.nv25.10 | 10.13.3.9 | 1.1.0 | 13.0.2.006 | 580.95.05 | 16.17 GB | | 25.12 | nvcr.io/nvidia/tritonserver:25.12-trtllm-python-py3 | Python 3.12.3 | 2.9.0a0+145a3a7bda.nv25.10 | 10.13.3.9 | 1.1.0 | 13.0.2.006 | 580.95.05 | 16.04 GB | | 25.11 | nvcr.io/nvidia/tritonserver:25.11-trtllm-python-py3 | Python 3.12.3 | 2.9.0a0+145a3a7bda.nv25.10 | 10.13.3.9 | 1.0.3.2510 | 13.0.2.006 | 580.95.05 | 12.25 GB | @@ -65,6 +66,7 @@ | Triton release version | NGC Tag | Python version | vLLM version | CUDA version | CUDA Driver version | Size | | --- | --- | --- | --- | --- | --- | --- | +| 26.02 | nvcr.io/nvidia/tritonserver:26.02-vllm-python-py3 | Python 3.12.3 | 0.15.1+nv26.2 | 13.1.1.006 | 590.48.01 | 8.9G | | 26.01 | nvcr.io/nvidia/tritonserver:26.01-vllm-python-py3 | Python 3.12.3 | 0.13.0+faa43dbf.nv26.1.cu131 | 13.1.1.006 | 590.48.01 | 8.79G | | 25.12 | nvcr.io/nvidia/tritonserver:25.12-vllm-python-py3 | Python 3.12.3 | 0.11.1+9114fd76.nv25.12.cu131 | 13.1.0.036 | 590.44.01 | 8.54G | | 25.11 | nvcr.io/nvidia/tritonserver:25.11-vllm-python-py3 | Python 3.12.3 | 0.11.0+582e4e37.nv25.11.cu130 | 13.0.2.006 | 580.95.05 | 8.72G | @@ -92,6 +94,7 @@ | Triton release version | ONNX Runtime | | --- | --- | +| 26.02 | 1.24.1 | | 26.01 | 1.23.2 | | 25.12 | 1.23.2 | | 25.11 | 1.23.2 | diff --git a/docs/introduction/release_notes.md b/docs/introduction/release_notes.md index 6aa87882eb..c0410b3a75 100644 --- a/docs/introduction/release_notes.md +++ b/docs/introduction/release_notes.md @@ -25,9 +25,9 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --> -# [Triton Inference Server Release 26.01](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-26-01.html#rel-26-01) +# [Triton Inference Server Release 26.02](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-26-02.html#rel-26-02) -The Triton Inference Server container image, release 26.01, is available +The Triton Inference Server container image, release 26.02, is available on [NGC](https://ngc.nvidia.com/catalog/containers/nvidia:tritonserver) and is open source on [GitHub](https://github.com/triton-inference-server/server). Release notes can diff --git a/python/openai/README.md b/python/openai/README.md index 931dcb1b4e..945b050be0 100644 --- a/python/openai/README.md +++ b/python/openai/README.md @@ -46,7 +46,7 @@ docker run -it --net=host --gpus all --rm \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ -e HF_TOKEN \ - nvcr.io/nvidia/tritonserver:26.01-vllm-python-py3 + nvcr.io/nvidia/tritonserver:26.02-vllm-python-py3 ``` 2. Launch the OpenAI-compatible Triton Inference Server: @@ -355,7 +355,7 @@ Currently, OpenAI-Compatible Frontend supports loading embedding models and embe docker run -it --net=host --gpus all --rm \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ -e HF_TOKEN \ - nvcr.io/nvidia/tritonserver:26.01-vllm-python-py3 + nvcr.io/nvidia/tritonserver:26.02-vllm-python-py3 ``` 2. Launch the OpenAI-compatible Triton Inference Server: @@ -451,7 +451,7 @@ docker run -it --net=host --gpus all --rm \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ -e HF_TOKEN \ -e TRTLLM_ORCHESTRATOR=1 \ - nvcr.io/nvidia/tritonserver:26.01-trtllm-python-py3 + nvcr.io/nvidia/tritonserver:26.02-trtllm-python-py3 ``` 2. Install dependencies inside the container: diff --git a/qa/common/gen_jetson_trt_models b/qa/common/gen_jetson_trt_models index aa507807c3..ef6a178cab 100755 --- a/qa/common/gen_jetson_trt_models +++ b/qa/common/gen_jetson_trt_models @@ -34,7 +34,7 @@ # Make all generated files accessible outside of container umask 0000 # Set the version of the models -TRITON_VERSION=${TRITON_VERSION:=26.01} +TRITON_VERSION=${TRITON_VERSION:=26.02} # Set the CUDA device to use NVIDIA_VISIBLE_DEVICES=${NVIDIA_VISIBLE_DEVICES:=0} # Set TensorRT image diff --git a/qa/common/gen_qa_model_repository b/qa/common/gen_qa_model_repository index dc63c6daee..f7cd2b0cbe 100755 --- a/qa/common/gen_qa_model_repository +++ b/qa/common/gen_qa_model_repository @@ -60,7 +60,7 @@ log_message.status "Changing working directory to the script directory to: " "${ cd ${TRITON_MDLS_BASE_SCRIPT_DIR} log_message.status "define: default values" -TRITON_VERSION=${TRITON_VERSION:=26.01} +TRITON_VERSION=${TRITON_VERSION:=26.02} ONNX_VERSION=1.20.1 ONNX_OPSET=0 OPENVINO_VERSION=2024.5.0 From 04af2a624b6040a82f692a2dff176b71dde4098f Mon Sep 17 00:00:00 2001 From: Mudit Aggarwal Date: Wed, 4 Mar 2026 09:37:12 -0800 Subject: [PATCH 162/230] fix: Block path traversal attacks via the deployment API for MLflow-Triton (#8665) --- .../mlflow_triton/deployments.py | 19 ++++- qa/L0_mlflow/plugin_test.py | 69 +++++++++++++++++-- qa/L0_mlflow/test.sh | 6 +- 3 files changed, 83 insertions(+), 11 deletions(-) diff --git a/deploy/mlflow-triton-plugin/mlflow_triton/deployments.py b/deploy/mlflow-triton-plugin/mlflow_triton/deployments.py index bebe559b9e..ee56faf4af 100755 --- a/deploy/mlflow-triton-plugin/mlflow_triton/deployments.py +++ b/deploy/mlflow-triton-plugin/mlflow_triton/deployments.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2021-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -90,7 +90,7 @@ def create_deployment(self, name, model_uri, flavor=None, config=None): """ Deploy the model at the model_uri to the Triton model repo. Associated config.pbtxt and *labels* files will be deployed. - :param name: Name of the of the model + :param name: Name of the model :param model_uri: Model uri in format model:// :param flavor: Flavor of the deployed model :param config: Configuration parameters @@ -99,6 +99,9 @@ def create_deployment(self, name, model_uri, flavor=None, config=None): """ self._validate_flavor(flavor) + # Validate model name + self._validate_model_name(name) + # Verify model does not already exist in Triton if self._model_exists(name): raise Exception( @@ -513,6 +516,18 @@ def _validate_flavor(self, flavor): if flavor not in self.supported_flavors: raise Exception("{} model flavor not supported by Triton".format(flavor)) + def _validate_model_name(self, name): + # Check if the model name is empty or only contains whitespace, tabs, or newlines + if name.strip() == "": + raise Exception( + "Model name cannot be empty. Please enter a valid name to deploy." + ) + # Path traversal protection + if "/" in name or name == "..": + raise Exception( + "Path traversal is not allowed in model's name: {}".format(name) + ) + def _model_exists(self, name): deploys = self.list_deployments() exists = False diff --git a/qa/L0_mlflow/plugin_test.py b/qa/L0_mlflow/plugin_test.py index a5d87a3c19..e225ed5881 100755 --- a/qa/L0_mlflow/plugin_test.py +++ b/qa/L0_mlflow/plugin_test.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# Copyright 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -33,7 +33,9 @@ import json import unittest +import mlflow.onnx import numpy as np +import onnx import test_util as tu from mlflow.deployments import get_deploy_client @@ -45,7 +47,7 @@ def setUp(self): def _validate_deployment(self, model_name): # create self.client_.create_deployment( - model_name, "models:/{}/1".format(model_name), flavor="onnx" + model_name, f"models:/{model_name}/1", flavor="onnx" ) # list @@ -79,8 +81,6 @@ def _validate_deployment(self, model_name): def test_onnx_flavor(self): # Log the ONNX model to MLFlow - import mlflow.onnx - import onnx model = onnx.load( "./mlflow-triton-plugin/examples/onnx_float32_int32_int32/1/model.onnx" @@ -92,8 +92,6 @@ def test_onnx_flavor(self): def test_onnx_flavor_with_files(self): # Log the ONNX model and additional Triton config file to MLFlow - import mlflow.onnx - import onnx model = onnx.load( "./mlflow-triton-plugin/examples/onnx_float32_int32_int32/1/model.onnx" @@ -116,6 +114,65 @@ def test_onnx_flavor_with_files(self): filecmp.cmp(config_path, "./models/onnx_model_with_files/config.pbtxt") ) + def test_model_name(self): + EMPTY_MODEL_NAMES = [ + "", + " ", + " ", + "\n", + "\t", + "\r", + "\v", + "\f", + ] + INVALID_PATH_TRAVERSAL_NAMES = [ + "/opt/sys/", + "../../etc/passwd", + "../outside/repo", + "test_models/../identity_py", + "..", + ] + VALID_MODEL_NAMES = [ + "model123", + # "model OAI", TRI-769: Fix this test case + "model.version1", + "...", + "..my_model", + "model..1", + "model....1", + ] + + for model_name in EMPTY_MODEL_NAMES: + model_uri = f"models:/{model_name}/1" + with self.assertRaises(Exception) as e: + self.client_.create_deployment(model_name, model_uri, flavor="onnx") + self.assertIn( + "Model name cannot be empty. Please enter a valid name to deploy.", + str(e.exception), + ) + + for model_name in INVALID_PATH_TRAVERSAL_NAMES: + model_uri = f"models:/{model_name}/1" + with self.assertRaises(Exception) as e: + self.client_.create_deployment(model_name, model_uri, flavor="onnx") + self.assertIn( + f"Path traversal is not allowed in model's name: {model_name}", + str(e.exception), + ) + + for model_name in VALID_MODEL_NAMES: + model = onnx.load( + "./mlflow-triton-plugin/examples/onnx_float32_int32_int32/1/model.onnx" + ) + + # Use a different name to ensure the plugin operates on correct model + mlflow.onnx.log_model( + model, "triton", registered_model_name=f"{model_name}" + ) + + # Validate deployment functionalities - create, list, get, predict, delete + self._validate_deployment(model_name) + if __name__ == "__main__": unittest.main() diff --git a/qa/L0_mlflow/test.sh b/qa/L0_mlflow/test.sh index 1bf7bc18f8..5e863a8988 100755 --- a/qa/L0_mlflow/test.sh +++ b/qa/L0_mlflow/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -187,7 +187,7 @@ if [ $? -ne 0 ]; then echo -e "\n***\n*** Python Test Failed\n***" RET=1 else - check_test_results $TEST_RESULT_FILE 2 + check_test_results $TEST_RESULT_FILE 3 if [ $? -ne 0 ]; then cat $PY_LOG echo -e "\n***\n*** Test Result Verification Failed\n***" @@ -256,7 +256,7 @@ if [ $? -ne 0 ]; then echo -e "\n***\n*** Python Test Failed\n***" RET=1 else - check_test_results $TEST_RESULT_FILE 2 + check_test_results $TEST_RESULT_FILE 3 if [ $? -ne 0 ]; then cat $PY_LOG echo -e "\n***\n*** Test Result Verification Failed\n***" From 09062232aa6aeb68d7f0316278f75c10168ca4f7 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:34:53 -0800 Subject: [PATCH 163/230] Switch to stable API (#8684) --- qa/common/test_util.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/qa/common/test_util.py b/qa/common/test_util.py index ec8108072f..3eee61ce52 100755 --- a/qa/common/test_util.py +++ b/qa/common/test_util.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -313,16 +313,13 @@ def check_gpus_compute_capability(min_capability): import importlib.util if importlib.util.find_spec("cuda") is not None: - import cuda.core.experimental as cuda_core_experimental - - devices = cuda_core_experimental.system.devices + from cuda.core import Device + devices = Device.get_all_devices() for device in devices: - compute_capability = ( - device.compute_capability.major + device.compute_capability.minor / 10.0 - ) - - if compute_capability < min_capability: + cc = device.compute_capability + compute_capability_value = cc.major + cc.minor / 10.0 + if compute_capability_value < min_capability: return False elif importlib.util.find_spec("pycuda") is not None: From 952a9fbdb635682ca64c1b6d3008b15232673256 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Thu, 5 Mar 2026 12:18:44 -0800 Subject: [PATCH 164/230] test: Add L0_backend_onnxruntime test for enabling bfloat16 dtype in ONNXRuntime backend (#8660) --- .../gen_add_bf16_onnx_model.py | 100 ++++++++++++++++++ qa/L0_backend_onnxruntime/test.py | 81 ++++++++++++++ qa/L0_backend_onnxruntime/test.sh | 92 ++++++++++++++++ 3 files changed, 273 insertions(+) create mode 100755 qa/L0_backend_onnxruntime/gen_add_bf16_onnx_model.py create mode 100755 qa/L0_backend_onnxruntime/test.py create mode 100755 qa/L0_backend_onnxruntime/test.sh diff --git a/qa/L0_backend_onnxruntime/gen_add_bf16_onnx_model.py b/qa/L0_backend_onnxruntime/gen_add_bf16_onnx_model.py new file mode 100755 index 0000000000..e3fc39b38e --- /dev/null +++ b/qa/L0_backend_onnxruntime/gen_add_bf16_onnx_model.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +# Generates the add_bf16 ONNX model and Triton config. +# Model: element-wise Add in BFLOAT16 (INPUT0 + INPUT1 = OUTPUT), ONNX Runtime backend. +import os + +import onnx + + +def generate_bf16_add_model(models_dir): + """Generate a simple BFLOAT16 Add model (INPUT0 + INPUT1 = OUTPUT).""" + model_name = "add_bf16" + shape = [1] + onnx_dtype = onnx.TensorProto.BFLOAT16 + + add = onnx.helper.make_node("Add", ["INPUT0", "INPUT1"], ["OUTPUT"]) + + input0 = onnx.helper.make_tensor_value_info("INPUT0", onnx_dtype, shape) + input1 = onnx.helper.make_tensor_value_info("INPUT1", onnx_dtype, shape) + output = onnx.helper.make_tensor_value_info("OUTPUT", onnx_dtype, shape) + + graph_proto = onnx.helper.make_graph( + [add], + model_name, + [input0, input1], + [output], + ) + model_def = onnx.helper.make_model(graph_proto, producer_name="triton") + # Cap IR version for older ONNX Runtime (e.g. max supported 11) + model_def.ir_version = min(model_def.ir_version, 11) + # BFLOAT16 support requires opset 13+ + model_def.opset_import[0].version = 13 + + model_dir = os.path.join(models_dir, model_name, "1") + os.makedirs(model_dir, exist_ok=True) + onnx.save(model_def, os.path.join(model_dir, "model.onnx")) + + # Write config.pbtxt + config = """platform: "onnxruntime_onnx" +max_batch_size: 0 +input [ + {{ + name: "INPUT0" + data_type: TYPE_BF16 + dims: {shape} + }}, + {{ + name: "INPUT1" + data_type: TYPE_BF16 + dims: {shape} + }} +] +output [ + {{ + name: "OUTPUT" + data_type: TYPE_BF16 + dims: {shape} + }} +] +""".format( + shape=shape + ) + + config_path = os.path.join(models_dir, model_name, "config.pbtxt") + with open(config_path, "w") as f: + f.write(config) + + print(f"Generated model '{model_name}' in {models_dir}") + + +if __name__ == "__main__": + models_dir = os.path.join(os.getcwd(), "models") + os.makedirs(models_dir, exist_ok=True) + generate_bf16_add_model(models_dir) diff --git a/qa/L0_backend_onnxruntime/test.py b/qa/L0_backend_onnxruntime/test.py new file mode 100755 index 0000000000..1a5437cdce --- /dev/null +++ b/qa/L0_backend_onnxruntime/test.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import os +import unittest + +import numpy as np +import tritonclient.grpc as grpcclient +import tritonclient.http as httpclient + + +class BFloat16Test(unittest.TestCase): + def setUp(self): + self.protocol = os.environ.get("CLIENT_TYPE", "http") + if self.protocol == "http": + self.client_ = httpclient.InferenceServerClient("localhost:8000") + else: + self.client_ = grpcclient.InferenceServerClient("localhost:8001") + self.model_name_ = "add_bf16" + self.shape_ = [1] + + def _infer_bf16(self, input0_data, input1_data): + """Helper to run BF16 inference and return the output numpy array.""" + if self.protocol == "http": + input0 = httpclient.InferInput("INPUT0", self.shape_, "BF16") + input1 = httpclient.InferInput("INPUT1", self.shape_, "BF16") + else: + input0 = grpcclient.InferInput("INPUT0", self.shape_, "BF16") + input1 = grpcclient.InferInput("INPUT1", self.shape_, "BF16") + input0.set_data_from_numpy(input0_data) + input1.set_data_from_numpy(input1_data) + + results = self.client_.infer(self.model_name_, [input0, input1]) + return results.as_numpy("OUTPUT") + + def test_bf16_add_variants(self): + """Run BF16 add across multiple cases: zeros, negatives, large, small, cancellation, and identical.""" + for input0_val, input1_val, expected_val in [ + (0.0, 0.0, 0.0), # zeros + (-1.5, 3.5, 2.0), # negatives / mixed + (100.0, 200.0, 300.0), # large + (1e-2, 1e-2, 2e-2), # small (near underflow) + (1.0, -1.0, 0.0), # cancellation + (2.0, 2.0, 4.0), # identical inputs + ]: + output = self._infer_bf16( + np.full(self.shape_, input0_val, dtype=np.float32), + np.full(self.shape_, input1_val, dtype=np.float32), + ) + self.assertEqual(output.dtype, np.float32) + # TODO: BF16 to FP32 conversion loses precision. Remove rtol and atol in TRI-801. + # BF16 has ~3 decimal digits; use relaxed tol for computed values + np.testing.assert_allclose(output, expected_val, rtol=1e-2, atol=1e-3) + + +if __name__ == "__main__": + unittest.main() diff --git a/qa/L0_backend_onnxruntime/test.sh b/qa/L0_backend_onnxruntime/test.sh new file mode 100755 index 0000000000..f4e349c2be --- /dev/null +++ b/qa/L0_backend_onnxruntime/test.sh @@ -0,0 +1,92 @@ +#!/bin/bash +# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +export CUDA_VISIBLE_DEVICES=0 + +SERVER=/opt/tritonserver/bin/tritonserver +SERVER_LOG="./inference_server.log" +CLIENT_LOG="./test.log" +source ../common/util.sh + +rm -f *.log +rm -rf models + +RET=0 + +# BFLOAT16 test +# Generate the model +mkdir -p models/add_bf16/1 +set +e + +pip install onnx==1.20.1 +if [ $? -ne 0 ]; then + echo -e "\n***\n*** Failed to install onnx dependency\n***" + exit 1 +fi + +python gen_add_bf16_onnx_model.py +if [ $? -ne 0 ]; then + echo -e "\n***\n*** Failed to generate BFLOAT16 ONNX model\n***" + exit 1 +fi + +set -e + +SERVER_ARGS="--model-repository=`pwd`/models" +run_server +if [ "$SERVER_PID" == "0" ]; then + echo -e "\n***\n*** Failed to start $SERVER\n***" + cat $SERVER_LOG + exit 1 +fi + +set +e + +for client_type in http grpc; do + export CLIENT_TYPE=$client_type + CLIENT_LOG="./test_${client_type}.log" + python test.py >>$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed ($client_type)\n***" + RET=1 + fi +done +unset CLIENT_TYPE + +set -e + +kill $SERVER_PID +wait $SERVER_PID + +if [ $RET -eq 0 ]; then + echo -e "\n***\n*** Test Passed\n***" +else + echo -e "\n***\n*** Test FAILED\n***" +fi + +exit $RET From 6b0539b922afdb21a9a09a2dd1a55709bf105756 Mon Sep 17 00:00:00 2001 From: Sai Kiran Polisetty Date: Sat, 7 Mar 2026 17:38:59 +0530 Subject: [PATCH 165/230] fix: Update Vertex AI endpoint to use HTTP restriction config (#8680) --- docs/customization_guide/deploy.md | 6 +- .../inference_protocols.md | 5 + qa/L0_vertex_ai/test.sh | 151 ++++++++++++++++-- src/main.cc | 17 +- src/vertex_ai_server.cc | 12 +- src/vertex_ai_server.h | 6 +- 6 files changed, 178 insertions(+), 19 deletions(-) diff --git a/docs/customization_guide/deploy.md b/docs/customization_guide/deploy.md index 36b048da35..2a305de5d3 100644 --- a/docs/customization_guide/deploy.md +++ b/docs/customization_guide/deploy.md @@ -1,5 +1,5 @@ - export RELEASE="24.07" + export RELEASE="26.03" docker run -it --net=host --gpus '"device=0"' nvcr.io/nvidia/tritonserver:${RELEASE}-py3-sdk 17. ## Download the Phi-3 tokenizer diff --git a/docs/introduction/compatibility.md b/docs/introduction/compatibility.md index 3e6840bfde..0c2156ecb0 100644 --- a/docs/introduction/compatibility.md +++ b/docs/introduction/compatibility.md @@ -38,6 +38,7 @@ | Triton release version | NGC Tag | Python version | Torch version | TensorRT version | TensorRT-LLM version | CUDA version | CUDA Driver version | Size | | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 26.03 | nvcr.io/nvidia/tritonserver:26.03-trtllm-python-py3 | Python 3.12.3 | 2.10.0a0+b4e4ee81d3.nv25.12 | 0.14.1.48 | 1.2.0 | 13.1.0.036 | 590.44.01 | 14.18 GB | | 26.02 | nvcr.io/nvidia/tritonserver:26.02-trtllm-python-py3 | Python 3.12.3 | 2.9.0a0+145a3a7bda.nv25.10 | 10.13.3.9 | 1.1.0 | 13.0.2.006 | 580.95.05 | 16.17 GB | | 26.01 | nvcr.io/nvidia/tritonserver:26.01-trtllm-python-py3 | Python 3.12.3 | 2.9.0a0+145a3a7bda.nv25.10 | 10.13.3.9 | 1.1.0 | 13.0.2.006 | 580.95.05 | 16.17 GB | | 25.12 | nvcr.io/nvidia/tritonserver:25.12-trtllm-python-py3 | Python 3.12.3 | 2.9.0a0+145a3a7bda.nv25.10 | 10.13.3.9 | 1.1.0 | 13.0.2.006 | 580.95.05 | 16.04 GB | @@ -66,6 +67,7 @@ | Triton release version | NGC Tag | Python version | vLLM version | CUDA version | CUDA Driver version | Size | | --- | --- | --- | --- | --- | --- | --- | +| 26.03 | nvcr.io/nvidia/tritonserver:26.03-vllm-python-py3 | Python 3.12.3 | 0.17.1+fb2e3ab6.nv26.3.46332470.cu132 | 13.2.0.046 | 595.45.04 | 9.22G | | 26.02 | nvcr.io/nvidia/tritonserver:26.02-vllm-python-py3 | Python 3.12.3 | 0.15.1+nv26.2 | 13.1.1.006 | 590.48.01 | 8.9G | | 26.01 | nvcr.io/nvidia/tritonserver:26.01-vllm-python-py3 | Python 3.12.3 | 0.13.0+faa43dbf.nv26.1.cu131 | 13.1.1.006 | 590.48.01 | 8.79G | | 25.12 | nvcr.io/nvidia/tritonserver:25.12-vllm-python-py3 | Python 3.12.3 | 0.11.1+9114fd76.nv25.12.cu131 | 13.1.0.036 | 590.44.01 | 8.54G | @@ -94,6 +96,7 @@ | Triton release version | ONNX Runtime | | --- | --- | +| 26.03 | 1.24.2 | | 26.02 | 1.24.1 | | 26.01 | 1.23.2 | | 25.12 | 1.23.2 | diff --git a/docs/introduction/release_notes.md b/docs/introduction/release_notes.md index c0410b3a75..acb78e6f1e 100644 --- a/docs/introduction/release_notes.md +++ b/docs/introduction/release_notes.md @@ -25,9 +25,9 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --> -# [Triton Inference Server Release 26.02](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-26-02.html#rel-26-02) +# [Triton Inference Server Release 26.03](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-26-03.html#rel-26-03) -The Triton Inference Server container image, release 26.02, is available +The Triton Inference Server container image, release 26.03, is available on [NGC](https://ngc.nvidia.com/catalog/containers/nvidia:tritonserver) and is open source on [GitHub](https://github.com/triton-inference-server/server). Release notes can diff --git a/docs/user_guide/performance_tuning.md b/docs/user_guide/performance_tuning.md index 9264307617..ecc95126bd 100644 --- a/docs/user_guide/performance_tuning.md +++ b/docs/user_guide/performance_tuning.md @@ -1,5 +1,5 @@ - export RELEASE="26.03" + export RELEASE="26.04" docker run -it --net=host --gpus '"device=0"' nvcr.io/nvidia/tritonserver:${RELEASE}-py3-sdk 17. ## Download the Phi-3 tokenizer diff --git a/docs/introduction/compatibility.md b/docs/introduction/compatibility.md index 0c2156ecb0..63398d3d08 100644 --- a/docs/introduction/compatibility.md +++ b/docs/introduction/compatibility.md @@ -38,7 +38,8 @@ | Triton release version | NGC Tag | Python version | Torch version | TensorRT version | TensorRT-LLM version | CUDA version | CUDA Driver version | Size | | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| 26.03 | nvcr.io/nvidia/tritonserver:26.03-trtllm-python-py3 | Python 3.12.3 | 2.10.0a0+b4e4ee81d3.nv25.12 | 0.14.1.48 | 1.2.0 | 13.1.0.036 | 590.44.01 | 14.18 GB | +| 26.04 | nvcr.io/nvidia/tritonserver:26.04-trtllm-python-py3 | Python 3.12.3 | 2.10.0a0+b4e4ee81d3.nv25.12 | 10.14.1.48 | 1.2.1 | 13.1.0.036 | 590.44.01 | 14.22 GB | +| 26.03 | nvcr.io/nvidia/tritonserver:26.03-trtllm-python-py3 | Python 3.12.3 | 2.10.0a0+b4e4ee81d3.nv25.12 | 10.14.1.48 | 1.2.0 | 13.1.0.036 | 590.44.01 | 14.18 GB | | 26.02 | nvcr.io/nvidia/tritonserver:26.02-trtllm-python-py3 | Python 3.12.3 | 2.9.0a0+145a3a7bda.nv25.10 | 10.13.3.9 | 1.1.0 | 13.0.2.006 | 580.95.05 | 16.17 GB | | 26.01 | nvcr.io/nvidia/tritonserver:26.01-trtllm-python-py3 | Python 3.12.3 | 2.9.0a0+145a3a7bda.nv25.10 | 10.13.3.9 | 1.1.0 | 13.0.2.006 | 580.95.05 | 16.17 GB | | 25.12 | nvcr.io/nvidia/tritonserver:25.12-trtllm-python-py3 | Python 3.12.3 | 2.9.0a0+145a3a7bda.nv25.10 | 10.13.3.9 | 1.1.0 | 13.0.2.006 | 580.95.05 | 16.04 GB | @@ -67,6 +68,7 @@ | Triton release version | NGC Tag | Python version | vLLM version | CUDA version | CUDA Driver version | Size | | --- | --- | --- | --- | --- | --- | --- | +| 26.04 | nvcr.io/nvidia/tritonserver:26.04-vllm-python-py3 | Python 3.12.3 | 0.19.0+6bc3197f.nv26.04.48761268 | 13.2.1.009 | 595.58.03 | 9.09G | | 26.03 | nvcr.io/nvidia/tritonserver:26.03-vllm-python-py3 | Python 3.12.3 | 0.17.1+fb2e3ab6.nv26.3.46332470.cu132 | 13.2.0.046 | 595.45.04 | 9.22G | | 26.02 | nvcr.io/nvidia/tritonserver:26.02-vllm-python-py3 | Python 3.12.3 | 0.15.1+nv26.2 | 13.1.1.006 | 590.48.01 | 8.9G | | 26.01 | nvcr.io/nvidia/tritonserver:26.01-vllm-python-py3 | Python 3.12.3 | 0.13.0+faa43dbf.nv26.1.cu131 | 13.1.1.006 | 590.48.01 | 8.79G | @@ -96,6 +98,7 @@ | Triton release version | ONNX Runtime | | --- | --- | +| 26.04 | 1.24.4 | | 26.03 | 1.24.2 | | 26.02 | 1.24.1 | | 26.01 | 1.23.2 | diff --git a/docs/introduction/release_notes.md b/docs/introduction/release_notes.md index acb78e6f1e..295734b89f 100644 --- a/docs/introduction/release_notes.md +++ b/docs/introduction/release_notes.md @@ -25,9 +25,9 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --> -# [Triton Inference Server Release 26.03](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-26-03.html#rel-26-03) +# [Triton Inference Server Release 26.04](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-26-04.html#rel-26-04) -The Triton Inference Server container image, release 26.03, is available +The Triton Inference Server container image, release 26.04, is available on [NGC](https://ngc.nvidia.com/catalog/containers/nvidia:tritonserver) and is open source on [GitHub](https://github.com/triton-inference-server/server). Release notes can diff --git a/docs/user_guide/performance_tuning.md b/docs/user_guide/performance_tuning.md index ecc95126bd..d51ad5ab2f 100644 --- a/docs/user_guide/performance_tuning.md +++ b/docs/user_guide/performance_tuning.md @@ -235,7 +235,7 @@ with a `tritonserver` binary. ```bash # Start server container -docker run -ti --rm --gpus=all --network=host -v $PWD:/mnt --name triton-server nvcr.io/nvidia/tritonserver:26.03-py3 +docker run -ti --rm --gpus=all --network=host -v $PWD:/mnt --name triton-server nvcr.io/nvidia/tritonserver:26.04-py3 # Start serving your models tritonserver --model-repository=/mnt/models @@ -284,7 +284,7 @@ by setting the `-u` flag, such as `perf_analyzer -m densenet_onnx -u ```bash # Start the SDK container interactively -docker run -ti --rm --gpus=all --network=host -v $PWD:/mnt --name triton-client nvcr.io/nvidia/tritonserver:26.03-py3-sdk +docker run -ti --rm --gpus=all --network=host -v $PWD:/mnt --name triton-client nvcr.io/nvidia/tritonserver:26.04-py3-sdk # Benchmark model being served from step 3 perf_analyzer -m densenet_onnx --concurrency-range 1:4 diff --git a/python/openai/README.md b/python/openai/README.md index abd3cd7af1..4598a5a43f 100644 --- a/python/openai/README.md +++ b/python/openai/README.md @@ -46,7 +46,7 @@ docker run -it --net=host --gpus all --rm \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ -e HF_TOKEN \ - nvcr.io/nvidia/tritonserver:26.03-vllm-python-py3 + nvcr.io/nvidia/tritonserver:26.04-vllm-python-py3 ``` 2. Launch the OpenAI-compatible Triton Inference Server: @@ -355,7 +355,7 @@ Currently, OpenAI-Compatible Frontend supports loading embedding models and embe docker run -it --net=host --gpus all --rm \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ -e HF_TOKEN \ - nvcr.io/nvidia/tritonserver:26.03-vllm-python-py3 + nvcr.io/nvidia/tritonserver:26.04-vllm-python-py3 ``` 2. Launch the OpenAI-compatible Triton Inference Server: @@ -451,7 +451,7 @@ docker run -it --net=host --gpus all --rm \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ -e HF_TOKEN \ -e TRTLLM_ORCHESTRATOR=1 \ - nvcr.io/nvidia/tritonserver:26.03-trtllm-python-py3 + nvcr.io/nvidia/tritonserver:26.04-trtllm-python-py3 ``` 2. Install dependencies inside the container: diff --git a/qa/common/gen_jetson_trt_models b/qa/common/gen_jetson_trt_models index 883b63ea91..7fcdb49823 100755 --- a/qa/common/gen_jetson_trt_models +++ b/qa/common/gen_jetson_trt_models @@ -34,7 +34,7 @@ # Make all generated files accessible outside of container umask 0000 # Set the version of the models -TRITON_VERSION=${TRITON_VERSION:=26.03} +TRITON_VERSION=${TRITON_VERSION:=26.04} # Set the CUDA device to use NVIDIA_VISIBLE_DEVICES=${NVIDIA_VISIBLE_DEVICES:=0} # Set TensorRT image diff --git a/qa/common/gen_qa_model_repository b/qa/common/gen_qa_model_repository index 796ad4a3df..328f42bbe0 100755 --- a/qa/common/gen_qa_model_repository +++ b/qa/common/gen_qa_model_repository @@ -66,7 +66,7 @@ log_message.status "Changing working directory to the script directory to: " "${ cd ${TRITON_MDLS_BASE_SCRIPT_DIR} log_message.status "define: default values" -TRITON_VERSION=${TRITON_VERSION:=26.03} +TRITON_VERSION=${TRITON_VERSION:=26.04} ONNX_VERSION=1.20.1 ONNX_OPSET=0 OPENVINO_VERSION=2024.5.0 From f1602005f3f5e6f0d75dc4fcce110c552dba8566 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Mon, 4 May 2026 18:14:30 -0700 Subject: [PATCH 206/230] fix: tag tritonfrontend wheel with arch-specific platform tag (#8761) --- build.py | 13 ++++ src/python/build_wheel.py | 152 ++++++++++++++++++++++++++++++++++++-- src/python/setup.py | 31 +++----- 3 files changed, 170 insertions(+), 26 deletions(-) diff --git a/build.py b/build.py index cb3490bf0d..557f55ae77 100755 --- a/build.py +++ b/build.py @@ -972,6 +972,13 @@ def create_dockerfile_buildbase_rhel(ddir, dockerfile_name, argmap): xz-devel \\ zlib-devel """ + if argmap["NVIDIA_BUILD_ID"] is not None: + df += """ +ENV BUILD_NUMBER={} +""".format( + argmap["NVIDIA_BUILD_ID"] + ) + if os.getenv("CCACHE_REMOTE_ONLY") and os.getenv("CCACHE_REMOTE_STORAGE"): df += """ RUN curl -k -s -L https://github.com/ccache/ccache/archive/refs/tags/v4.10.2.tar.gz -o /tmp/ccache.tar.gz \\ @@ -1049,6 +1056,12 @@ def create_dockerfile_buildbase(ddir, dockerfile_name, argmap): ARG TRITON_CONTAINER_VERSION ENV PIP_BREAK_SYSTEM_PACKAGES=1 CMAKE_POLICY_VERSION_MINIMUM=3.5 """ + if argmap["NVIDIA_BUILD_ID"] is not None: + df += """ +ENV BUILD_NUMBER={} +""".format( + argmap["NVIDIA_BUILD_ID"] + ) # Install the windows- or linux-specific buildbase dependencies if target_platform() == "windows": df += """ diff --git a/src/python/build_wheel.py b/src/python/build_wheel.py index 875dd32a70..27edac6ec5 100755 --- a/src/python/build_wheel.py +++ b/src/python/build_wheel.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -32,7 +32,6 @@ import shutil import subprocess import sys -from distutils.dir_util import copy_tree from tempfile import mkstemp @@ -51,7 +50,7 @@ def touch(path): def cpdir(src, dest): - copy_tree(src, dest, preserve_symlinks=1) + shutil.copytree(src, dest, symlinks=True, dirs_exist_ok=True) def sed(pattern, replace, source, dest=None): @@ -70,6 +69,127 @@ def sed(pattern, replace, source, dest=None): shutil.copyfile(name, source) +def _detect_cuda_version() -> str | None: + """Detect the CUDA toolkit version visible to the build. + + Prefers the CUDA_VERSION env var (set by official NVIDIA base + images); falls back to parsing /usr/local/cuda/version.json which + is the canonical location for the installed toolkit. + + Returns: + str or None: The CUDA version as a string (e.g. "13.2.1"), or None if CUDA is not available. + """ + v = os.environ.get("CUDA_VERSION") + if v: + return v + try: + import json as _json + + with open("/usr/local/cuda/version.json") as f: + data = _json.load(f) + return data.get("cuda", {}).get("version") + except (OSError, ValueError, KeyError): + return None + + +def _compose_version(base_version): + """Compose the full wheel version string. + + Appends a PEP 440 local-version segment describing the NVIDIA + container release and CUDA toolkit so consumers can tell an + nv26.04 wheel from an nv26.05 wheel and a cu132 wheel from a + cu128 wheel. All sources are optional; local non-CI builds return + the version unchanged. + """ + nv = ( + os.environ.get("NVIDIA_UPSTREAM_VERSION") + or os.environ.get("NVIDIA_TRITON_SERVER_VERSION") + or os.environ.get("TRITON_CONTAINER_VERSION") + ) + cuda = _detect_cuda_version() + print( + f"=== Wheel local-version inputs: " + f"NVIDIA_UPSTREAM_VERSION={os.environ.get('NVIDIA_UPSTREAM_VERSION')!r} " + f"NVIDIA_TRITON_SERVER_VERSION={os.environ.get('NVIDIA_TRITON_SERVER_VERSION')!r} " + f"TRITON_CONTAINER_VERSION={os.environ.get('TRITON_CONTAINER_VERSION')!r} " + f"-> nv={nv!r}, cuda={cuda!r}", + file=sys.stderr, + ) + local = [] + if nv: + local.append(f"nv{nv}") + if cuda: + parts = cuda.split(".") + if len(parts) >= 2 and parts[0].isdigit() and parts[1].isdigit(): + local.append(f"cu{parts[0]}{parts[1]}") + if local: + return f"{base_version}+{'.'.join(local)}" + return base_version + + +def _repair_wheel_with_auditwheel(whl_dir, dest_dir): + """Upgrade a linux_ wheel to manylinux_2_X_. + + Ports the pattern established for tritonclient in TRI-286: + 1. auditwheel repair — auto-discovers the minimum manylinux tag + by inspecting glibc symbol requirements of the embedded .so. + 2. python -m wheel tags fallback — used when auditwheel reports + "no ELF" (the wheel has no native extension, e.g. a downstream + build disabled bindings). Mirrors the documented fallback. + 3. No-op with warning — when auditwheel is not installed in the + build image, keep the linux_ wheel as-is so the build + does not regress. + """ + if shutil.which("auditwheel") is None: + print( + "=== WARNING: auditwheel not found on PATH; keeping linux_ " + "wheel as-is. Install auditwheel in the build image to produce " + "PyPI-acceptable manylinux_2_X_ wheels.", + file=sys.stderr, + ) + shutil.copytree(os.path.join(whl_dir, "dist"), dest_dir, dirs_exist_ok=True) + return + + dist_dir = os.path.join(whl_dir, "dist") + wheels = [ + os.path.join(dist_dir, w) for w in os.listdir(dist_dir) if w.endswith(".whl") + ] + fail_if(not wheels, "no wheel produced by the build") + + for wheel_path in wheels: + print(f"=== Running auditwheel repair on {wheel_path}") + r = subprocess.run( + ["auditwheel", "repair", wheel_path, "--wheel-dir", dest_dir], + capture_output=True, + text=True, + ) + if r.returncode != 0 and "no ELF" in r.stderr: + arch = os.uname().machine + manylinux_tag = f"manylinux_2_28_{arch}" + print( + f"=== Pure-Python wheel detected; falling back to wheel tags " + f"({manylinux_tag})" + ) + copied = os.path.join(dest_dir, os.path.basename(wheel_path)) + shutil.copy(wheel_path, copied) + r2 = subprocess.run( + [ + "python3", + "-m", + "wheel", + "tags", + "--platform-tag", + manylinux_tag, + "--remove", + copied, + ] + ) + fail_if(r2.returncode != 0, "wheel tags fallback failed") + elif r.returncode != 0: + sys.stderr.write(r.stderr) + fail_if(True, "auditwheel repair failed") + + def main(): parser = argparse.ArgumentParser() @@ -117,15 +237,37 @@ def main(): os.chdir(FLAGS.whl_dir) print("=== Building wheel") args = ["python3", "setup.py", "bdist_wheel"] + # PEP 427 build tag: lets two wheels of the same version coexist + # (e.g. reruns of the same CI pipeline). Sources, first non-empty + # and usable wins: + # CI_PIPELINE_ID - GitLab pipeline-scoped ID (preferred). + # NVIDIA_BUILD_ID - from build.py's --build-id flag. + # BUILD_NUMBER - generic CI systems. + # PEP 427 requires the build tag to start with a digit. + build_tag = ( + os.environ.get("CI_PIPELINE_ID") + or os.environ.get("NVIDIA_BUILD_ID") + or os.environ.get("BUILD_NUMBER") + ) + print( + f"=== Wheel build-tag inputs: " + f"CI_PIPELINE_ID={os.environ.get('CI_PIPELINE_ID')!r} " + f"NVIDIA_BUILD_ID={os.environ.get('NVIDIA_BUILD_ID')!r} " + f"BUILD_NUMBER={os.environ.get('BUILD_NUMBER')!r} " + f"-> build-tag={build_tag!r}", + file=sys.stderr, + ) + if build_tag and build_tag != "" and build_tag[:1].isdigit(): + args += [f"--build-number={build_tag}"] wenv = os.environ.copy() - wenv["VERSION"] = FLAGS.triton_version + wenv["VERSION"] = _compose_version(FLAGS.triton_version) wenv["TRITON_PYBIND"] = PYBIND_LIB p = subprocess.Popen(args, env=wenv) p.wait() fail_if(p.returncode != 0, "setup.py failed") - cpdir("dist", FLAGS.dest_dir) + _repair_wheel_with_auditwheel(FLAGS.whl_dir, FLAGS.dest_dir) print(f"=== Output wheel file is in: {FLAGS.dest_dir}") touch(os.path.join(FLAGS.dest_dir, "stamp.whl")) diff --git a/src/python/setup.py b/src/python/setup.py index 2c7c12a9ee..12cf7b9afb 100755 --- a/src/python/setup.py +++ b/src/python/setup.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -26,34 +26,23 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import os -import sys -from setuptools import find_packages, setup - -if "--plat-name" in sys.argv: - PLATFORM_FLAG = sys.argv[sys.argv.index("--plat-name") + 1] -else: - PLATFORM_FLAG = "any" +from setuptools import Distribution, find_packages, setup if "VERSION" not in os.environ: raise Exception("envvar VERSION must be specified") VERSION = os.environ["VERSION"] -try: - from wheel.bdist_wheel import bdist_wheel as _bdist_wheel - - class bdist_wheel(_bdist_wheel): - def finalize_options(self): - _bdist_wheel.finalize_options(self) - self.root_is_pure = False - def get_tag(self): - pyver, abi, plat = "py3", "none", PLATFORM_FLAG - return pyver, abi, plat +# The wheel ships an arch-specific pybind11 extension bundled via +# package_data. Without has_ext_modules()=True setuptools marks the +# wheel pure-Python (py3-none-any), which auditwheel rejects. +# See TRI-983. +class BinaryDistribution(Distribution): + def has_ext_modules(self): + return True -except ImportError: - bdist_wheel = None this_directory = os.path.abspath(os.path.dirname(__file__)) @@ -105,7 +94,7 @@ def get_tag(self): "": platform_package_data, }, zip_safe=False, - cmdclass={"bdist_wheel": bdist_wheel}, + distclass=BinaryDistribution, data_files=data_files, install_requires=["tritonserver", "pydantic==2.10.6"], extras_require={"GPU": gpu_extras, "test": test_extras, "all": all_extras}, From 69987b7b0f76539afdcf0ff893af5d68a0bb1fe8 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Wed, 6 May 2026 14:49:48 -0700 Subject: [PATCH 207/230] fix: Reject requests if parameters and forward headers contain Triton reserved parameter keys (#8763) --- docs/protocol/extension_parameters.md | 5 +- qa/L0_parameters/parameters_test.py | 421 +++++++++++++++++--------- qa/L0_parameters/test.sh | 24 +- src/common.h | 12 +- src/grpc/infer_handler.cc | 6 +- src/grpc/infer_handler.h | 19 +- src/http_server.cc | 22 +- 7 files changed, 344 insertions(+), 165 deletions(-) diff --git a/docs/protocol/extension_parameters.md b/docs/protocol/extension_parameters.md index eef7aeb11b..17edcf2776 100644 --- a/docs/protocol/extension_parameters.md +++ b/docs/protocol/extension_parameters.md @@ -42,11 +42,12 @@ The following parameters are reserved for Triton's usage and should not be used as custom parameters: - sequence_id -- priority -- timeout - sequence_start - sequence_end +- priority +- timeout - headers +- binary_data_output - All the keys that start with `"triton_"` prefix. Some examples used today: - `"triton_enable_empty_final_response"` request parameter - `"triton_final_response"` response parameter diff --git a/qa/L0_parameters/parameters_test.py b/qa/L0_parameters/parameters_test.py index a20d13c1eb..90221c12d7 100755 --- a/qa/L0_parameters/parameters_test.py +++ b/qa/L0_parameters/parameters_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2023-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -30,66 +30,86 @@ sys.path.append("../common") -import os import queue import unittest from functools import partial -from unittest import IsolatedAsyncioTestCase import numpy as np +import requests import tritonclient.grpc as grpcclient import tritonclient.grpc.aio as asyncgrpcclient import tritonclient.http as httpclient import tritonclient.http.aio as asynchttpclient from tritonclient.utils import InferenceServerException -TEST_HEADER = os.environ.get("TEST_HEADER") +_TRITON_RESERVED_CLIENT_ERROR = "is a reserved parameter and cannot be specified" +_TRITON_RESERVED_SERVER_ERROR = "reserved for Triton usage" + +# docs/protocol/extension_parameters.md — reserved names +_RESERVED_PARAMETER_KEYS = ( + "sequence_id", + "sequence_start", + "sequence_end", + "priority", + "timeout", + "headers", + "binary_data_output", + "triton_enable_empty_final_response", + "triton_final_response", + "triton_injected_via_header", +) + +_HTTP_LOCALHOST = "http://localhost:8000" + + +def _infer_url(model_name="parameter"): + return f"{_HTTP_LOCALHOST}/v2/models/{model_name}/infer" + + +def _minimal_fp32_infer_body(parameters=None): + """KServe HTTP infer JSON matching qa/L0_parameters model `parameter` (INPUT0 FP32 [1]).""" + body = { + "inputs": [ + { + "name": "INPUT0", + "shape": [1], + "datatype": "FP32", + "data": [1.0], + } + ], + } + if parameters is not None: + body["parameters"] = parameters + return body -class InferenceParametersTest(IsolatedAsyncioTestCase): - async def asyncSetUp(self): - self.http = httpclient.InferenceServerClient(url="localhost:8000") - self.async_http = asynchttpclient.InferenceServerClient(url="localhost:8000") - self.grpc = grpcclient.InferenceServerClient(url="localhost:8001") - self.async_grpc = asyncgrpcclient.InferenceServerClient(url="localhost:8001") - - self.parameter_list = [] - self.parameter_list.append({"key1": "value1", "key2": "value2"}) - self.parameter_list.append({"key1": 1, "key2": 2}) - self.parameter_list.append({"key1": 123.123, "key2": 321.321}) - self.parameter_list.append({"key1": True, "key2": "value2"}) - self.parameter_list.append({"triton_": True, "key2": "value2"}) - - # Only "test_params" tests parameters without headers. - if TEST_HEADER != "test_params": - self.headers = { - "header_1": "value_1", - "header_2": "value_2", - "my_header_1": "my_value_1", - "my_header_2": "my_value_2", - "my_header_3": 'This is a "quoted" string with a backslash\ ', - } +_FORWARD_HEADERS = { + "header_1": "value_1", + "header_2": "value_2", + "my_header_1": "my_value_1", + "my_header_2": "my_value_2", + "my_header_3": 'This is a "quoted" string with a backslash\\ ', +} - # only these headers should be forwarded to the model. - if TEST_HEADER == "test_grpc_header_forward_pattern_case_sensitive": - self.expected_headers = {} - else: - self.expected_headers = { - "my_header_1": "my_value_1", - "my_header_2": "my_value_2", - "my_header_3": 'This is a "quoted" string with a backslash\ ', - } - else: - self.headers = {} - self.expected_headers = {} - def callback(user_data, result, error): - if error: - user_data.put(error) - else: - user_data.put(result) +def _grpc_stream_callback(user_data, result, error): + if error: + user_data.put(error) + else: + user_data.put(result) - self.grpc_callback = callback + +class InferenceParametersTest(unittest.IsolatedAsyncioTestCase): + async def asyncSetUp(self): + self.httpclient = httpclient.InferenceServerClient(url="localhost:8000") + self.async_httpclient = asynchttpclient.InferenceServerClient( + url="localhost:8000" + ) + self.grpcclient = grpcclient.InferenceServerClient(url="localhost:8001") + self.async_grpcclient = asyncgrpcclient.InferenceServerClient( + url="localhost:8001" + ) + self.grpcclient_callback = _grpc_stream_callback def create_inputs(self, client_type): inputs = [] @@ -100,128 +120,257 @@ def create_inputs(self, client_type): return inputs async def send_request_and_verify( - self, client_type, client, is_async=False, model_name="parameter" + self, + client_type, + client, + parameters, + headers, + expected_headers, + is_async_infer=False, + model_name="parameter", ): inputs = self.create_inputs(client_type) - for parameters in self.parameter_list: - # Setup infer callable to re-use below for brevity + + if is_async_infer: + if client_type == httpclient: + result = client.async_infer( + model_name=model_name, + inputs=inputs, + parameters=parameters, + headers=headers, + ).get_result() + elif client_type == grpcclient: + user_data = queue.Queue() + client.async_infer( + model_name=model_name, + inputs=inputs, + parameters=parameters, + headers=headers, + callback=partial(self.grpcclient_callback, user_data), + ) + result = user_data.get() + self.assertIsNot(result, InferenceServerException) + else: + raise ValueError(f"Unsupported client type: {client_type}") + else: infer_callable = partial( client.infer, model_name=model_name, inputs=inputs, parameters=parameters, - headers=self.headers, + headers=headers, ) - - # The `triton_` prefix is reserved for Triton usage - should_error = False - if "triton_" in parameters.keys(): - should_error = True - - if is_async: - if should_error: - with self.assertRaises(InferenceServerException): - await infer_callable() - return - else: - result = await infer_callable() + if client_type == asynchttpclient or client_type == asyncgrpcclient: + result = await infer_callable() else: - if should_error: - with self.assertRaises(InferenceServerException): - infer_callable() - return - else: - result = infer_callable() + result = infer_callable() - self.verify_outputs(result, parameters) + self.verify_outputs(result, parameters, expected_headers) - def verify_outputs(self, result, parameters): + def verify_outputs(self, result, parameters, expected_headers): keys = result.as_numpy("key") values = result.as_numpy("value") keys = keys.astype(str).tolist() - expected_keys = list(parameters.keys()) + list(self.expected_headers.keys()) - self.assertEqual(set(keys), set(expected_keys)) + expected_keys = list(parameters.keys()) + list(expected_headers.keys()) + self.assertEqual( + set(keys), + set(expected_keys), + msg=f"keys: {keys}, expected_keys: {expected_keys}", + ) # We have to convert the parameter values to string expected_values = [] for expected_value in list(parameters.values()): expected_values.append(str(expected_value)) - for value in self.expected_headers.values(): + for value in expected_headers.values(): expected_values.append(value) - self.assertEqual(set(values.astype(str).tolist()), set(expected_values)) + self.assertEqual( + set(values.astype(str).tolist()), + set(expected_values), + msg=f"values: {values.astype(str).tolist()}, expected_values: {expected_values}", + ) - async def test_grpc_parameter(self): - await self.send_request_and_verify(grpcclient, self.grpc) + async def _verify_grpc_stream_infer(self, parameters, headers, expected_headers): + user_data = queue.Queue() + self.grpcclient.start_stream( + callback=partial(self.grpcclient_callback, user_data), headers=headers + ) + inputs = self.create_inputs(grpcclient) + self.grpcclient.async_stream_infer( + model_name="parameter", inputs=inputs, parameters=parameters + ) + result = user_data.get() + self.assertIsNot(result, InferenceServerException) + self.verify_outputs(result, parameters, expected_headers) + self.grpcclient.stop_stream() + + def _raw_http_post_infer(self, body_dict, extra_headers=None): + """POST /v2/models/.../infer without tritonclient (no header normalization).""" + headers = {"Content-Type": "application/json"} + if extra_headers: + headers.update(extra_headers) + return requests.post( + _infer_url("parameter"), + json=body_dict, + headers=headers, + timeout=60, + ) - async def test_http_parameter(self): - await self.send_request_and_verify(httpclient, self.http) + def _assert_raw_http_400(self, response, text_substr): + """Assert status 400 and body contains `text_substr`.""" + snippet = response.text[:2000] if response.text else "" + self.assertEqual(response.status_code, 400, msg=snippet) + self.assertIn(text_substr, response.text, msg=snippet) - async def test_async_http_parameter(self): + async def _run_client_infer_suite(self, parameters, headers, expected_headers): + """ + Full client matrix: gRPC/HTTP sync+async, stream, ensemble. + """ await self.send_request_and_verify( - asynchttpclient, self.async_http, is_async=True + grpcclient, self.grpcclient, parameters, headers.copy(), expected_headers ) - - async def test_async_grpc_parameter(self): await self.send_request_and_verify( - asyncgrpcclient, self.async_grpc, is_async=True + httpclient, self.httpclient, parameters, headers.copy(), expected_headers ) - - def test_http_async_parameter(self): - inputs = self.create_inputs(httpclient) - # Skip the parameter that returns an error - parameter_list = self.parameter_list[:-1] - for parameters in parameter_list: - result = self.http.async_infer( - model_name="parameter", - inputs=inputs, - parameters=parameters, - headers=self.headers, - ).get_result() - self.verify_outputs(result, parameters) - - def test_grpc_async_parameter(self): - user_data = queue.Queue() - inputs = self.create_inputs(grpcclient) - # Skip the parameter that returns an error - parameter_list = self.parameter_list[:-1] - for parameters in parameter_list: - self.grpc.async_infer( - model_name="parameter", - inputs=inputs, - parameters=parameters, - headers=self.headers, - callback=partial(self.grpc_callback, user_data), - ) - result = user_data.get() - self.assertFalse(result is InferenceServerException) - self.verify_outputs(result, parameters) - - def test_grpc_stream_parameter(self): - user_data = queue.Queue() - self.grpc.start_stream( - callback=partial(self.grpc_callback, user_data), headers=self.headers + await self.send_request_and_verify( + asynchttpclient, + self.async_httpclient, + parameters, + headers.copy(), + expected_headers, + ) + await self.send_request_and_verify( + asyncgrpcclient, + self.async_grpcclient, + parameters, + headers.copy(), + expected_headers, + ) + await self.send_request_and_verify( + httpclient, + self.httpclient, + parameters, + headers.copy(), + expected_headers, + is_async_infer=True, + ) + await self.send_request_and_verify( + grpcclient, + self.grpcclient, + parameters, + headers.copy(), + expected_headers, + is_async_infer=True, + ) + await self._verify_grpc_stream_infer( + parameters, headers.copy(), expected_headers + ) + await self.send_request_and_verify( + httpclient, + self.httpclient, + parameters, + headers.copy(), + expected_headers, + model_name="ensemble", ) - inputs = self.create_inputs(grpcclient) - # Skip the parameter that returns an error - parameter_list = self.parameter_list[:-1] - for parameters in parameter_list: - # async stream infer - self.grpc.async_stream_infer( - model_name="parameter", inputs=inputs, parameters=parameters - ) - result = user_data.get() - self.assertFalse(result is InferenceServerException) - self.verify_outputs(result, parameters) - self.grpc.stop_stream() - async def test_ensemble_parameter_forwarding(self): - await self.send_request_and_verify(httpclient, self.http, model_name="ensemble") + async def test_params(self): + for parameters in [ + {"key1": "value1", "key2": "value2"}, + {"key1": 1, "key2": 2}, + {"key1": 123.123, "key2": 321.321}, + {"key1": True, "key2": "value2"}, + ]: + await self._run_client_infer_suite(parameters, {}, {}) + + async def test_params_reserved_rejected(self): + for reserved_key in _RESERVED_PARAMETER_KEYS: + parameters = {reserved_key: "dummy-value"} + body = _minimal_fp32_infer_body(parameters) + + # Raw HTTP + if reserved_key.startswith("triton_"): + r = self._raw_http_post_infer(body) + self._assert_raw_http_400(r, _TRITON_RESERVED_SERVER_ERROR) + + # Python clients + for client_type, client in ( + (httpclient, self.httpclient), + (asynchttpclient, self.async_httpclient), + (grpcclient, self.grpcclient), + (asyncgrpcclient, self.async_grpcclient), + ): + inputs = self.create_inputs(client_type) + with self.assertRaises(InferenceServerException) as cm: + if client_type in (asynchttpclient, asyncgrpcclient): + await client.infer( + model_name="parameter", + inputs=inputs, + parameters=parameters, + ) + else: + client.infer( + model_name="parameter", + inputs=inputs, + parameters=parameters, + ) + msg = str(cm.exception) + # Reserved parameters are rejected by the client + self.assertIn(_TRITON_RESERVED_CLIENT_ERROR, msg, msg=msg) + + async def test_headers(self): + expected_headers = { + "my_header_1": "my_value_1", + "my_header_2": "my_value_2", + "my_header_3": 'This is a "quoted" string with a backslash\\ ', + } + await self._run_client_infer_suite({}, _FORWARD_HEADERS, expected_headers) + + async def test_grpc_header_forward_pattern_case_sensitive(self): + expected_headers = {} + await self._run_client_infer_suite({}, _FORWARD_HEADERS, expected_headers) + + async def test_headers_reserved_rejected(self): + body = _minimal_fp32_infer_body({}) + for reserved_key in _RESERVED_PARAMETER_KEYS: + # Raw HTTP + r = self._raw_http_post_infer( + body, extra_headers={reserved_key: "dummy-value"} + ) + self._assert_raw_http_400(r, _TRITON_RESERVED_SERVER_ERROR) + + # Python clients + for client_type, client in ( + (httpclient, self.httpclient), + (asynchttpclient, self.async_httpclient), + (grpcclient, self.grpcclient), + (asyncgrpcclient, self.async_grpcclient), + ): + inputs = self.create_inputs(client_type) + with self.assertRaises(InferenceServerException) as cm: + if client_type in (asynchttpclient, asyncgrpcclient): + await client.infer( + model_name="parameter", + inputs=inputs, + parameters={}, + headers={reserved_key: "dummy-value"}, + ) + else: + client.infer( + model_name="parameter", + inputs=inputs, + parameters={}, + headers={reserved_key: "dummy-value"}, + ) + msg = str(cm.exception) + # Headers are not rejected by the client + self.assertIn(_TRITON_RESERVED_SERVER_ERROR, msg, msg=msg) async def asyncTearDown(self): - self.http.close() - self.grpc.close() - await self.async_grpc.close() - await self.async_http.close() + self.httpclient.close() + self.grpcclient.close() + await self.async_grpcclient.close() + await self.async_httpclient.close() if __name__ == "__main__": diff --git a/qa/L0_parameters/test.sh b/qa/L0_parameters/test.sh index b4d1599754..a6ca428c4b 100755 --- a/qa/L0_parameters/test.sh +++ b/qa/L0_parameters/test.sh @@ -39,8 +39,6 @@ if [ ! -z "$TEST_REPO_ARCH" ]; then fi CLIENT_LOG="./client.log" -TEST_SCRIPT_PY="parameters_test.py" - SERVER=/opt/tritonserver/bin/tritonserver SERVER_LOG="./inference_server.log" source ../common/util.sh @@ -54,27 +52,27 @@ mkdir -p "${MODELDIR}/ensemble/1" # https://jirasw.nvidia.com/browse/DLIS-4673 all_tests=("test_params" + "test_params_reserved_rejected" "test_headers" - "test_header_forward_pattern_case_insensitive" - "test_grpc_header_forward_pattern_case_sensitive") + "test_grpc_header_forward_pattern_case_sensitive" + "test_headers_reserved_rejected") RET=0 -for i in "${all_tests[@]}"; do - # TEST_HEADER is a parameter used by `parameters_test.py` that controls - # whether the script will test for inclusion of headers in parameters or not. +for test in "${all_tests[@]}"; do SERVER_ARGS="--model-repository=${MODELDIR} --exit-timeout-secs=120" - if [ "$i" == "test_headers" ]; then - SERVER_ARGS+=" --grpc-header-forward-pattern my_header.*" - SERVER_ARGS+=" --http-header-forward-pattern my_header.*" - elif [ "$i" == "test_header_forward_pattern_case_insensitive" ]; then + if [ "$test" == "test_headers" ]; then + # gRPC lowercases metadata keys; HTTP may lowercase too — match prefix case-insensitively. SERVER_ARGS+=" --grpc-header-forward-pattern MY_HEADER.*" SERVER_ARGS+=" --http-header-forward-pattern MY_HEADER.*" # NOTE: headers sent through the python HTTP client may be automatically # lowercased by internal libraries like geventhttpclient, so we only test # GRPC client for case-sensitivity here: # https://github.com/geventhttpclient/geventhttpclient/blob/d1e14356c3b02099c879cf9b3bdb684a0cbd8bf5/src/geventhttpclient/header.py#L62-L63 - elif [ "$i" == "test_grpc_header_forward_pattern_case_sensitive" ]; then + elif [ "$test" == "test_grpc_header_forward_pattern_case_sensitive" ]; then SERVER_ARGS+=" --grpc-header-forward-pattern (?-i)MY_HEADER.*" + elif [ "$test" == "test_headers_reserved_rejected" ]; then + SERVER_ARGS+=" --grpc-header-forward-pattern .*" + SERVER_ARGS+=" --http-header-forward-pattern .*" fi run_server if [ "$SERVER_PID" == "0" ]; then @@ -84,7 +82,7 @@ for i in "${all_tests[@]}"; do fi set +e - TEST_HEADER="$i" python3 $TEST_SCRIPT_PY >$CLIENT_LOG 2>&1 + python3 -m unittest "parameters_test.InferenceParametersTest.${test}" >$CLIENT_LOG 2>&1 if [ $? -ne 0 ]; then cat $CLIENT_LOG echo -e "\n***\n*** Test Failed\n***" diff --git a/src/common.h b/src/common.h index fbd5a4b276..c49f79c12b 100644 --- a/src/common.h +++ b/src/common.h @@ -25,10 +25,12 @@ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #pragma once +#include #include #include #include #include +#include #include #include #include @@ -66,7 +68,15 @@ constexpr int32_t HTTP_MAX_JSON_NESTING_DEPTH = 100; // Default maximum allowed HTTP request input size in bytes (64MB) constexpr size_t HTTP_DEFAULT_MAX_INPUT_SIZE = 1 << 26; -/// Request parameter keys that start with a "triton_" prefix for internal use +/// Reserved parameter keys for Triton usage (also HTTP/gRPC header forward). +/// Other locations: +/// - client/src/python/library/tritonclient/utils/__init__.py +/// - server/docs/protocol/extension_parameters.md +constexpr std::array kReservedParameterKeys{ + "sequence_id", "sequence_start", "sequence_end", "priority", + "timeout", "headers", "binary_data_output"}; + +// Request parameter keys that start with a "triton_" prefix for internal use const std::vector TRITON_RESERVED_REQUEST_PARAMS{ "triton_enable_empty_final_response"}; diff --git a/src/grpc/infer_handler.cc b/src/grpc/infer_handler.cc index 2166f0c60e..b04f1b31b9 100644 --- a/src/grpc/infer_handler.cc +++ b/src/grpc/infer_handler.cc @@ -1,4 +1,4 @@ -// Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -291,8 +291,8 @@ SetInferenceRequestMetadata( TRITONSERVER_ERROR_INVALID_ARG, (std::string( "parameter keys starting with 'triton_' are reserved for " - "Triton " - "usage. Only the following keys starting with 'triton_' are " + "Triton usage. Only the following keys starting with " + "'triton_' are " "allowed: ") + Join(TRITON_RESERVED_REQUEST_PARAMS, " ")) .c_str()); diff --git a/src/grpc/infer_handler.h b/src/grpc/infer_handler.h index 2cd1ddae38..7e2abfd0fa 100644 --- a/src/grpc/infer_handler.h +++ b/src/grpc/infer_handler.h @@ -1601,11 +1601,22 @@ InferHandler:: for (const auto& pair : metadata) { auto& key = pair.first; auto& value = pair.second; - std::string param_key = std::string(key.begin(), key.end()); - if (RE2::PartialMatch(param_key, header_forward_regex_)) { - std::string param_value = std::string(value.begin(), value.end()); + std::string header_key = std::string(key.begin(), key.end()); + if (RE2::PartialMatch(header_key, header_forward_regex_)) { + if (std::find( + kReservedParameterKeys.begin(), kReservedParameterKeys.end(), + header_key) != kReservedParameterKeys.end() || + header_key.rfind("triton_", 0) == 0) { + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + ("Header '" + header_key + + "' is reserved for Triton usage and cannot be forwarded.") + .c_str()); + } + + std::string header_value = std::string(value.begin(), value.end()); err = TRITONSERVER_InferenceRequestSetStringParameter( - irequest, param_key.c_str(), param_value.c_str()); + irequest, header_key.c_str(), header_value.c_str()); if (err != nullptr) { break; } diff --git a/src/http_server.cc b/src/http_server.cc index 5cce136ede..f41cb6e9dc 100644 --- a/src/http_server.cc +++ b/src/http_server.cc @@ -2888,8 +2888,7 @@ HTTPAPIServer::ParseJsonTritonParams( return TRITONSERVER_ErrorNew( TRITONSERVER_ERROR_INVALID_ARG, ("parameter keys starting with 'triton_' are reserved for Triton " - "usage " - "and should not be specified.")); + "usage and should not be specified.")); } else { RETURN_IF_ERR(SetTritonParameterFromJsonParameter( parameter, params_json, irequest)); @@ -3192,14 +3191,25 @@ struct HeaderSearchPayload { int ForEachHeader(evhtp_header_t* header, void* arg) { - HeaderSearchPayload* header_search_payload = - reinterpret_cast(arg); + auto* header_search_payload = reinterpret_cast(arg); TRITONSERVER_InferenceRequest* request = header_search_payload->request_; const re2::RE2& regex = header_search_payload->regex_; + std::string header_key{header->key}; + + if (RE2::PartialMatch(header_key, regex)) { + if (std::find( + kReservedParameterKeys.begin(), kReservedParameterKeys.end(), + header_key) != kReservedParameterKeys.end() || + header_key.rfind("triton_", 0) == 0) { + header_search_payload->error_ = TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + ("Header '" + header_key + + "' is reserved for Triton usage and cannot be forwarded.") + .c_str()); + return 1; + } - std::string matched_string; - if (RE2::PartialMatch(std::string(header->key), regex)) { header_search_payload->error_ = TRITONSERVER_InferenceRequestSetStringParameter( request, header->key, header->val); From 2f65837f726a022028105eab740108d1681ccc11 Mon Sep 17 00:00:00 2001 From: Sai Kiran Polisetty Date: Thu, 7 May 2026 09:55:09 +0530 Subject: [PATCH 208/230] fix: Pre-allocate serialized buffer for gRPC BYTES input (#8769) --- qa/L0_grpc/python_unit_test.py | 60 +++++++++++++++++++++++++++++++++- qa/L0_grpc/test.sh | 35 +++++++++++++++++--- src/grpc/infer_handler.cc | 11 ++++++- 3 files changed, 100 insertions(+), 6 deletions(-) diff --git a/qa/L0_grpc/python_unit_test.py b/qa/L0_grpc/python_unit_test.py index 116f3b422e..5ed87078cf 100755 --- a/qa/L0_grpc/python_unit_test.py +++ b/qa/L0_grpc/python_unit_test.py @@ -34,7 +34,10 @@ import numpy as np import tritonclient.grpc as grpcclient -from tritonclient.utils import InferenceServerException +from tritonclient.grpc import service_pb2, service_pb2_grpc +from tritonclient.utils import InferenceServerException, deserialize_bytes_tensor + +import grpc class UserData: @@ -77,6 +80,61 @@ def test_duplicate_output_names_rejected(self): "Server is not healthy after duplicate output request", ) + def test_bytes_contents_many_elements_serialization(self): + """ + Regression test for InferGRPCToInput bytes_contents pre-allocation. + Sends a BYTES tensor with many explicit bytes_contents elements over + the raw gRPC stub. + """ + # 10,485,760 elements * (4-byte length + 1-byte payload) = 50 MiB + # of serialized BYTES data on both the request and response sides. + element_count = 10 * 1024 * 1024 + payload = b"A" + expected_serialized_size = element_count * (4 + len(payload)) + + request = service_pb2.ModelInferRequest() + request.model_name = "string_identity" + + input_tensor = request.inputs.add() + input_tensor.name = "INPUT0" + input_tensor.datatype = "BYTES" + input_tensor.shape.extend([element_count]) + input_tensor.contents.bytes_contents.extend([payload] * element_count) + request.outputs.add().name = "OUTPUT0" + + # The default Python gRPC client send/receive limit is 4 MiB which + # is below the request and response sizes used here. + channel_options = [ + ("grpc.max_send_message_length", 256 * 1024 * 1024), + ("grpc.max_receive_message_length", 256 * 1024 * 1024), + ] + grpc_stub = service_pb2_grpc.GRPCInferenceServiceStub( + grpc.insecure_channel("localhost:8001", options=channel_options) + ) + response = grpc_stub.ModelInfer(request, timeout=120) + + self.assertEqual(list(response.outputs[0].shape), [element_count]) + self.assertEqual(len(response.raw_output_contents[0]), expected_serialized_size) + + # Verify every element round-tripped correctly through the + # serialization path. + output = deserialize_bytes_tensor(response.raw_output_contents[0]) + self.assertEqual(output.shape, (element_count,)) + self.assertTrue( + np.all(output == payload), + "bytes_contents elements did not round-trip correctly", + ) + + client = grpcclient.InferenceServerClient(url="localhost:8001") + self.assertTrue( + client.is_server_live(), + "Server must remain healthy after many bytes_contents elements", + ) + self.assertTrue( + client.is_model_ready("string_identity"), + "Model must remain ready after many bytes_contents elements", + ) + class RestrictedProtocolTest(unittest.TestCase): def setUp(self): diff --git a/qa/L0_grpc/test.sh b/qa/L0_grpc/test.sh index f09857567f..c17c3db30c 100755 --- a/qa/L0_grpc/test.sh +++ b/qa/L0_grpc/test.sh @@ -152,6 +152,18 @@ sed -i "/CONTROL_SEQUENCE_CORRID/{n;s/data_type:.*/data_type: TYPE_STRING/}" ${M rm -f ${MODELDIR}/simple_string_dyna_sequence/1/model.onnx cp ../custom_models/custom_dyna_sequence_int32/1/libtriton_dyna_sequence.so ${MODELDIR}/simple_string_dyna_sequence/1/ +# Prepare a dedicated model repository for GrpcTest in python_unit_test.py. +# separate from MODELDIR avoids changing the model count expected by other +# tests in this script. +GRPC_TEST_MODELDIR=`pwd`/grpc_test_models +rm -rf ${GRPC_TEST_MODELDIR} +mkdir -p ${GRPC_TEST_MODELDIR} +cp -r ${MODELDIR}/simple ${GRPC_TEST_MODELDIR}/ +cp -r ../python_models/string_identity ${GRPC_TEST_MODELDIR}/string_identity +mkdir -p ${GRPC_TEST_MODELDIR}/string_identity/1 +mv ${GRPC_TEST_MODELDIR}/string_identity/model.py ${GRPC_TEST_MODELDIR}/string_identity/1/model.py +sed -i "s/dims: \[ 1 \]/dims: [ -1 ]/g" ${GRPC_TEST_MODELDIR}/string_identity/config.pbtxt + rm -f *.log rm -f *.log.* @@ -349,11 +361,26 @@ if [ $? -ne 0 ]; then RET=1 fi -# Test duplicate output names are rejected -python $PYTHON_UNIT_TEST GrpcTest >> ${CLIENT_LOG}.duplicate_output 2>&1 +set -e +kill $SERVER_PID +wait $SERVER_PID + +# Test duplicate output and bytes_contents +SERVER_ARGS="--backend-directory=${BACKEND_DIR} --model-repository=${GRPC_TEST_MODELDIR}" +SERVER_LOG=./inference_server_grpc_test.log +GRPC_TEST_CLIENT_LOG=./inference_client_grpc_test.log +run_server +if [ "$SERVER_PID" == "0" ]; then + echo -e "\n***\n*** Failed to start $SERVER\n***" + cat $SERVER_LOG + exit 1 +fi + +set +e +python $PYTHON_UNIT_TEST GrpcTest >> $GRPC_TEST_CLIENT_LOG 2>&1 if [ $? -ne 0 ]; then - cat ${CLIENT_LOG}.duplicate_output - echo -e "\n***\n*** Python GRPC Duplicate Output Test Failed\n***" + cat $GRPC_TEST_CLIENT_LOG + echo -e "\n***\n*** Python GRPC Test Failed\n***" RET=1 fi diff --git a/src/grpc/infer_handler.cc b/src/grpc/infer_handler.cc index b04f1b31b9..db50c5051a 100644 --- a/src/grpc/infer_handler.cc +++ b/src/grpc/infer_handler.cc @@ -587,7 +587,16 @@ InferGRPCToInput( serialized_data->emplace_back(); auto& serialized = serialized_data->back(); - // Serialize the output tensor strings. Each string is + // Pre-compute the total serialized byte size and reserve once to + // avoid repeated std::string reallocations and heap fragmentation + // when 'bytes_contents' carries a large number of elements. + size_t serialized_byte_size = 0; + for (const auto& element : io.contents().bytes_contents()) { + serialized_byte_size += sizeof(uint32_t) + element.size(); + } + serialized.reserve(serialized_byte_size); + + // Serialize the input tensor strings. Each string is // serialized as a 4-byte length followed by the string itself // with no null-terminator. for (const auto& element : io.contents().bytes_contents()) { From 669cef0832525591ab23b53606b719a5629e5b80 Mon Sep 17 00:00:00 2001 From: Sai Kiran Polisetty Date: Thu, 7 May 2026 13:46:20 +0530 Subject: [PATCH 209/230] fix: Prevent memory retention on failed compressed HTTP requests (#8764) --- qa/L0_http/http_input_size_limit_test.py | 70 ++++++++++++++++++++++++ qa/L0_http/test.sh | 10 +++- src/http_server.cc | 18 +++--- src/http_server.h | 13 ++++- 4 files changed, 100 insertions(+), 11 deletions(-) diff --git a/qa/L0_http/http_input_size_limit_test.py b/qa/L0_http/http_input_size_limit_test.py index 25f08071ab..e38edf9a9d 100755 --- a/qa/L0_http/http_input_size_limit_test.py +++ b/qa/L0_http/http_input_size_limit_test.py @@ -33,9 +33,11 @@ import gzip import io import json +import os import unittest import numpy as np +import psutil import requests import test_util as tu @@ -64,6 +66,18 @@ class InferSizeLimitTest(tu.TestResultCollector): def _get_infer_url(self, model_name): return f"http://localhost:8000/v2/models/{model_name}/infer" + def _get_server_process(self): + """ + Return a psutil.Process for the tritonserver under test. + """ + pid_str = os.environ.get("SERVER_PID") + if not pid_str: + raise AssertionError("SERVER_PID env var is not set") + try: + return psutil.Process(int(pid_str)) + except (ValueError, psutil.NoSuchProcess) as e: + raise AssertionError(f"Invalid or stale SERVER_PID={pid_str!r}: {e}") + def test_json_dtype_size_expansion_exceeds_limit_error(self): """ Test that when the client sends a JSON input of byte[], that when it @@ -735,6 +749,62 @@ def test_large_input_compressed(self): result = response.json() self.assertIn("outputs", result, "Response missing outputs field") + def test_no_leak_on_invalid_inference_header_length(self): + """ + Test that sending multiple malformed compressed requests does not cause memory growth on the server. + """ + leak_request_count = 100 + max_rss_growth_bytes = 32 * MB + model = "onnx_zero_1_float32" + + body = gzip.compress(b" " * MB) + headers = { + "Content-Type": "application/json", + "Content-Encoding": "gzip", + "Inference-Header-Content-Length": "9999999", + } + url = self._get_infer_url(model) + + server = self._get_server_process() + + with requests.Session() as session: + # Warm up the failure path so one-time allocations do not look + # like leaks. + resp = session.post(url, data=body, headers=headers) + self.assertEqual( + 400, + resp.status_code, + f"Expected status code 400, got {resp.status_code}: " + f"{resp.content[:200]!r}", + ) + + rss_before = server.memory_info().rss + for _ in range(leak_request_count): + resp = session.post(url, data=body, headers=headers) + self.assertEqual( + 400, + resp.status_code, + f"Expected status code 400, got {resp.status_code}: " + f"{resp.content[:200]!r}", + ) + rss_after = server.memory_info().rss + + growth = rss_after - rss_before + print( + f"RSS: before={rss_before / MB:.1f} MiB, " + f"after={rss_after / MB:.1f} MiB, " + f"growth={growth / MB:.1f} MiB, " + f"limit={max_rss_growth_bytes / MB:.0f} MiB", + flush=True, + ) + self.assertLess( + growth, + max_rss_growth_bytes, + f"Server RSS grew by {growth / MB:.1f} MiB after " + f"{leak_request_count} malformed compressed requests " + f"(limit {max_rss_growth_bytes / MB:.0f} MiB).", + ) + if __name__ == "__main__": unittest.main() diff --git a/qa/L0_http/test.sh b/qa/L0_http/test.sh index e11cbde38b..c66300b543 100755 --- a/qa/L0_http/test.sh +++ b/qa/L0_http/test.sh @@ -695,7 +695,7 @@ wait $SERVER_PID # Helper library to parse SSE events # https://github.com/mpetazzoni/sseclient -pip install sseclient-py +pip install sseclient-py psutil SERVER_ARGS="--model-repository=`pwd`/../python_models/generate_models --log-verbose=1" SERVER_LOG="./inference_server_generate_endpoint_test.log" @@ -852,6 +852,14 @@ if [ $? -ne 0 ]; then echo -e "\n***\n*** Default Input Size Limit Test Failed for type size explosion\n***" RET=1 fi + +# Test that sending multiple malformed compressed requests does not cause memory leaks on the server. +SERVER_PID=$SERVER_PID python http_input_size_limit_test.py InferSizeLimitTest.test_no_leak_on_invalid_inference_header_length >> $CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Decompression Leak Regression Failed\n***\n***" + RET=1 +fi set -e kill $SERVER_PID diff --git a/src/http_server.cc b/src/http_server.cc index f41cb6e9dc..d083f4da9f 100644 --- a/src/http_server.cc +++ b/src/http_server.cc @@ -3268,15 +3268,15 @@ HTTPAPIServer::StartTrace( TRITONSERVER_Error* HTTPAPIServer::DecompressBuffer( - evhtp_request_t* req, evbuffer** decompressed_buffer) + evhtp_request_t* req, EvbufferUniquePtr& decompressed_buffer) { auto compression_type = GetRequestCompressionType(req); switch (compression_type) { case DataCompressor::Type::DEFLATE: case DataCompressor::Type::GZIP: { - *decompressed_buffer = evbuffer_new(); + decompressed_buffer.reset(evbuffer_new()); RETURN_IF_ERR(DataCompressor::DecompressData( - compression_type, req->buffer_in, *decompressed_buffer, + compression_type, req->buffer_in, decompressed_buffer.get(), max_input_size_)); break; } @@ -3787,13 +3787,13 @@ HTTPAPIServer::HandleInfer( } // Decompress request body if it is compressed in supported type - evbuffer* decompressed_buffer = nullptr; - RETURN_AND_RESPOND_IF_ERR(req, DecompressBuffer(req, &decompressed_buffer)); + EvbufferUniquePtr decompressed_buffer; + RETURN_AND_RESPOND_IF_ERR(req, DecompressBuffer(req, decompressed_buffer)); // Get content length as a default header_length if no header specified int32_t content_length = 0; RETURN_AND_RESPOND_IF_ERR( - req, GetContentLength(req, decompressed_buffer, &content_length)); + req, GetContentLength(req, decompressed_buffer.get(), &content_length)); // Get the header length size_t header_length = 0; @@ -3845,8 +3845,8 @@ HTTPAPIServer::HandleInfer( // Parse EV request and fill Triton request fields from it RETURN_AND_CALLBACK_IF_ERR( EVRequestToTritonRequest( - req, model_name, irequest, decompressed_buffer, infer_request.get(), - header_length), + req, model_name, irequest, decompressed_buffer.get(), + infer_request.get(), header_length), error_callback); // Get request ID for logging in case of error. @@ -3861,7 +3861,7 @@ HTTPAPIServer::HandleInfer( RETURN_AND_CALLBACK_IF_ERR(ForwardHeaders(req, irequest), error_callback); auto request_release_payload = std::make_unique( - irequest_shared, decompressed_buffer); + irequest_shared, decompressed_buffer.release()); RETURN_AND_CALLBACK_IF_ERR( TRITONSERVER_InferenceRequestSetReleaseCallback( irequest, InferRequestClass::InferRequestComplete, diff --git a/src/http_server.h b/src/http_server.h index 2187ec1700..2a5ce0ef4a 100644 --- a/src/http_server.h +++ b/src/http_server.h @@ -475,6 +475,17 @@ class HTTPAPIServer : public HTTPServer { bool end_{false}; }; + struct ReleaseEvbuffer { + // Custom deleter for evbuffer + void operator()(evbuffer* b) const noexcept + { + if (b != nullptr) { + evbuffer_free(b); + } + } + }; + using EvbufferUniquePtr = std::unique_ptr; + // Simple structure that carries the userp payload needed for // request release callback. struct RequestReleasePayload final { @@ -535,7 +546,7 @@ class HTTPAPIServer : public HTTPServer { evhtp_request_t* req, evbuffer* decompressed_buffer, int32_t* content_length); TRITONSERVER_Error* DecompressBuffer( - evhtp_request_t* req, evbuffer** decompressed_buffer); + evhtp_request_t* req, EvbufferUniquePtr& decompressed_buffer); TRITONSERVER_Error* CheckTransactionPolicy( evhtp_request_t* req, const std::string& model_name, int64_t requested_model_version); From 13480cbf300ddf622093644fc7373a70aba4f2d1 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Fri, 8 May 2026 11:02:18 -0700 Subject: [PATCH 210/230] fix: Cap chunked HTTP request chunk count at 65536 to prevent memory exhaustion (#8770) --- qa/L0_http/http_input_size_limit_test.py | 66 +++----- qa/L0_http/http_request_many_chunks.py | 157 ++++++++++-------- qa/L0_http/test.sh | 2 +- .../sagemaker_request_many_chunks.py | 110 ++++++++++-- qa/L0_sagemaker/test.sh | 4 +- qa/common/test_util.py | 40 +++++ src/http_server.cc | 42 +++++ src/http_server.h | 4 + 8 files changed, 302 insertions(+), 123 deletions(-) diff --git a/qa/L0_http/http_input_size_limit_test.py b/qa/L0_http/http_input_size_limit_test.py index e38edf9a9d..aed855dd69 100755 --- a/qa/L0_http/http_input_size_limit_test.py +++ b/qa/L0_http/http_input_size_limit_test.py @@ -33,13 +33,11 @@ import gzip import io import json -import os import unittest import numpy as np -import psutil import requests -import test_util as tu +from test_util import GIB, MIB, TestResultCollector, get_server_process_from_env # Constants for size calculations # Each FP32 value is 4 bytes, so we need to divide target byte sizes by 4 to get element counts @@ -47,10 +45,8 @@ BYTES_PER_INT64 = ( 8 # For the type size explosion test, we use int64 which is 8 bytes per element ) -MB = 2**20 # 1 MB = 1,048,576 bytes -GB = 2**30 # 1 GB = 1,073,741,824 bytes -DEFAULT_LIMIT_BYTES = 64 * MB # 64MB default limit -INCREASED_LIMIT_BYTES = 128 * MB # 128MB increased limit +DEFAULT_LIMIT_BYTES = 64 * MIB # 64MB default limit +INCREASED_LIMIT_BYTES = 128 * MIB # 128MB increased limit # Calculate element counts for size limits DEFAULT_LIMIT_ELEMENTS = DEFAULT_LIMIT_BYTES // BYTES_PER_FP32 # 16,777,216 elements @@ -62,22 +58,10 @@ OFFSET_ELEMENTS = 32 -class InferSizeLimitTest(tu.TestResultCollector): +class InferSizeLimitTest(TestResultCollector): def _get_infer_url(self, model_name): return f"http://localhost:8000/v2/models/{model_name}/infer" - def _get_server_process(self): - """ - Return a psutil.Process for the tritonserver under test. - """ - pid_str = os.environ.get("SERVER_PID") - if not pid_str: - raise AssertionError("SERVER_PID env var is not set") - try: - return psutil.Process(int(pid_str)) - except (ValueError, psutil.NoSuchProcess) as e: - raise AssertionError(f"Invalid or stale SERVER_PID={pid_str!r}: {e}") - def test_json_dtype_size_expansion_exceeds_limit_error(self): """ Test that when the client sends a JSON input of byte[], that when it @@ -201,7 +185,7 @@ def test_default_limit_raw_binary(self): DEFAULT_LIMIT_ELEMENTS + OFFSET_ELEMENTS, dtype=np.float32 ) input_bytes = large_input.tobytes() - assert len(input_bytes) > 64 * MB # Verify we're actually over the 64MB limit + assert len(input_bytes) > 64 * MIB # Verify we're actually over the 64MB limit headers = {"Inference-Header-Content-Length": "0"} response = requests.post( @@ -231,7 +215,7 @@ def test_default_limit_raw_binary(self): DEFAULT_LIMIT_ELEMENTS - OFFSET_ELEMENTS, dtype=np.float32 ) input_bytes = small_input.tobytes() - assert len(input_bytes) < 64 * MB # Verify we're actually under the 64MB limit + assert len(input_bytes) < 64 * MIB # Verify we're actually under the 64MB limit response = requests.post( self._get_infer_url(model), data=input_bytes, headers=headers @@ -276,7 +260,7 @@ def test_default_limit_json(self): ] } assert ( - shape_size * BYTES_PER_FP32 > 64 * MB + shape_size * BYTES_PER_FP32 > 64 * MIB ) # Verify we're actually over the 64MB limit headers = {"Content-Type": "application/json"} @@ -361,7 +345,9 @@ def test_large_input_raw_binary(self): INCREASED_LIMIT_ELEMENTS + OFFSET_ELEMENTS, dtype=np.float32 ) input_bytes = large_input.tobytes() - assert len(input_bytes) > 128 * MB # Verify we're actually over the 128MB limit + assert ( + len(input_bytes) > 128 * MIB + ) # Verify we're actually over the 128MB limit headers = {"Inference-Header-Content-Length": "0"} response = requests.post( @@ -392,7 +378,7 @@ def test_large_input_raw_binary(self): ) input_bytes = small_input.tobytes() assert ( - len(input_bytes) < 128 * MB + len(input_bytes) < 128 * MIB ) # Verify we're actually under the 128MB limit response = requests.post( @@ -438,7 +424,7 @@ def test_large_input_json(self): ] } assert ( - shape_size * BYTES_PER_FP32 > 128 * MB + shape_size * BYTES_PER_FP32 > 128 * MIB ) # Verify we're actually over the 128MB limit headers = {"Content-Type": "application/json"} @@ -519,7 +505,7 @@ def test_large_string_in_json(self): # Create a string that is larger (large payload about 2GB) than the default limit of 64MB # (2^31 + 64) elements * 1 bytes = 2GB + 64 bytes = 2,147,483,712 bytes - large_string_size = 2 * GB + 64 + large_string_size = 2 * GIB + 64 large_string = "A" * large_string_size payload = { @@ -602,7 +588,7 @@ def test_default_limit_compressed(self): } # Test case 1: Payload that decompresses to 64MB + 1MB (over limit) should fail - large_target_size = DEFAULT_LIMIT_BYTES + MB + large_target_size = DEFAULT_LIMIT_BYTES + MIB ( large_compressed_data, large_uncompressed_size, @@ -642,7 +628,7 @@ def test_default_limit_compressed(self): ) # Test case 2: Payload that decompresses to 64MB - 1MB (under limit) should succeed - small_target_size = DEFAULT_LIMIT_BYTES - MB + small_target_size = DEFAULT_LIMIT_BYTES - MIB ( small_compressed_data, small_uncompressed_size, @@ -684,7 +670,7 @@ def test_large_input_compressed(self): } # Test case 1: Input that decompresses to 128MB + 1MB (over limit) should fail - large_target_size = INCREASED_LIMIT_BYTES + MB + large_target_size = INCREASED_LIMIT_BYTES + MIB ( large_compressed_data, large_uncompressed_size, @@ -716,7 +702,7 @@ def test_large_input_compressed(self): ) # Test case 2: Input that decompresses to 128MB - 1MB (under limit) should succeed - small_target_size = INCREASED_LIMIT_BYTES - MB + small_target_size = INCREASED_LIMIT_BYTES - MIB ( small_compressed_data, small_uncompressed_size, @@ -754,10 +740,10 @@ def test_no_leak_on_invalid_inference_header_length(self): Test that sending multiple malformed compressed requests does not cause memory growth on the server. """ leak_request_count = 100 - max_rss_growth_bytes = 32 * MB + max_rss_growth_bytes = 32 * MIB model = "onnx_zero_1_float32" - body = gzip.compress(b" " * MB) + body = gzip.compress(b" " * MIB) headers = { "Content-Type": "application/json", "Content-Encoding": "gzip", @@ -765,7 +751,7 @@ def test_no_leak_on_invalid_inference_header_length(self): } url = self._get_infer_url(model) - server = self._get_server_process() + server = get_server_process_from_env() with requests.Session() as session: # Warm up the failure path so one-time allocations do not look @@ -791,18 +777,18 @@ def test_no_leak_on_invalid_inference_header_length(self): growth = rss_after - rss_before print( - f"RSS: before={rss_before / MB:.1f} MiB, " - f"after={rss_after / MB:.1f} MiB, " - f"growth={growth / MB:.1f} MiB, " - f"limit={max_rss_growth_bytes / MB:.0f} MiB", + f"RSS: before={rss_before / MIB:.1f} MIB, " + f"after={rss_after / MIB:.1f} MIB, " + f"growth={growth / MIB:.1f} MIB, " + f"limit={max_rss_growth_bytes / MIB:.0f} MIB", flush=True, ) self.assertLess( growth, max_rss_growth_bytes, - f"Server RSS grew by {growth / MB:.1f} MiB after " + f"Server RSS grew by {growth / MIB:.1f} MIB after " f"{leak_request_count} malformed compressed requests " - f"(limit {max_rss_growth_bytes / MB:.0f} MiB).", + f"(limit {max_rss_growth_bytes / MIB:.0f} MIB).", ) diff --git a/qa/L0_http/http_request_many_chunks.py b/qa/L0_http/http_request_many_chunks.py index a67d9a82c5..d733fb000c 100755 --- a/qa/L0_http/http_request_many_chunks.py +++ b/qa/L0_http/http_request_many_chunks.py @@ -1,5 +1,5 @@ #!/usr/bin/python -# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -26,23 +26,34 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import socket +import sys import unittest +sys.path.append("../common") +from test_util import MIB, get_server_process_from_env, wait_for_stable_rss + class HTTPRequestManyChunksTest(unittest.TestCase): def setUp(self): - self._model_name = "simple" self._local_host = "localhost" self._http_port = 8000 - self._malicious_chunk_count = ( - 1000000 # large enough to cause a stack overflow if using alloca() - ) - self._parse_error = ( - "failed to parse the request JSON buffer: Invalid value. at 0" + self._model_name = "simple" + # Must match server kMaxChunkedChunks (http_server.cc). + self._k_max_chunked_chunks = 65536 + self._over_max_chunks_error = f"Chunked request body exceeds maximum of {self._k_max_chunked_chunks} non-empty chunks. Send fewer or larger HTTP chunks." + + def _infer_chunked_header(self): + return ( + f"POST /v2/models/{self._model_name}/infer HTTP/1.1\r\n" + f"Inference-Header-Content-Length: 0\r\n" ) def send_chunked_request( - self, header: str, chunk_count: int, expected_response: str + self, + header: str, + chunk_count: int, + expected_response: str, + expected_http_status=400, ): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) header = ( @@ -60,11 +71,17 @@ def send_chunked_request( # Send chunked payload for _ in range(chunk_count): - s.send(b"1\r\nA\r\n") - # End chunked encoding - s.sendall(b"0\r\n\r\n") + try: + s.send(b"1\r\nA\r\n") + except (BrokenPipeError, ConnectionResetError): + break + try: + s.sendall(b"0\r\n\r\n") + except (BrokenPipeError, ConnectionResetError): + # Server may close/reset early after deciding on an error response. + # Ignore send failure here and continue reading any available response bytes. + pass - # Receive response response = b"" while True: try: @@ -72,80 +89,88 @@ def send_chunked_request( if not chunk: break response += chunk + except ConnectionResetError: + break except socket.timeout: break + self.assertTrue( + response, + "expected error response body, but socket closed/reset before any bytes", + ) + status_line = response.split(b"\r\n", 1)[0].decode(errors="replace") + self.assertTrue( + status_line.startswith(f"HTTP/1.1 {expected_http_status} "), + f"expected HTTP status {expected_http_status}, got {status_line!r}", + ) self.assertIn(expected_response, response.decode()) except Exception as e: raise (e) finally: s.close() - def test_infer(self): - request_header = ( - f"POST /v2/models/{self._model_name}/infer HTTP/1.1\r\n" - f"Inference-Header-Content-Length: 0\r\n" - ) - - self.send_chunked_request( - request_header, - self._malicious_chunk_count, - "Raw request must only have 1 input (found 1) to be deduced but got 2 inputs in 'simple' model configuration", - ) - - def test_registry_index(self): - request_header = f"POST /v2/repository/index HTTP/1.1\r\n" - + def test_chunked_infer_at_max_chunks(self): + """Exactly kMaxChunkedChunks non-empty chunks: 400 request input error.""" self.send_chunked_request( - request_header, self._malicious_chunk_count, self._parse_error + self._infer_chunked_header(), + self._k_max_chunked_chunks, + "Raw request must only have 1 input (found 1) to be deduced but got 2 " + "inputs in 'simple' model configuration", ) - def test_model_control(self): - load_request_header = ( - f"POST /v2/repository/models/{self._model_name}/load HTTP/1.1\r\n" - ) - unload_request_header = load_request_header.replace("/load", "/unload") + def test_chunked_infer_rejected_over_max_chunks(self): + """kMaxChunkedChunks+1 chunks: 400 request error with bounded RSS growth.""" + # Warm up failure path to avoid one-time allocation noise in RSS checks. self.send_chunked_request( - load_request_header, self._malicious_chunk_count, self._parse_error - ) - self.send_chunked_request( - unload_request_header, self._malicious_chunk_count, self._parse_error + self._infer_chunked_header(), + self._k_max_chunked_chunks + 1, + self._over_max_chunks_error, ) - def test_trace(self): - request_header = ( - f"POST /v2/models/{self._model_name}/trace/setting HTTP/1.1\r\n" - ) + def test_chunked_infer_over_max_chunks_reject_with_bounded_rss_growth(self): + many_chunks = 1000000 - self.send_chunked_request( - request_header, self._malicious_chunk_count, self._parse_error - ) - - def test_logging(self): - request_header = f"POST /v2/logging HTTP/1.1\r\n" + # verify server is running + server = get_server_process_from_env("SERVER_PID") + self.assertTrue(server.is_running()) + # warm up and wait until RSS is stable. self.send_chunked_request( - request_header, self._malicious_chunk_count, self._parse_error + self._infer_chunked_header(), + many_chunks, # way over max chunks + self._over_max_chunks_error, ) - - def test_system_shm_register(self): - request_header = f"POST /v2/systemsharedmemory/region/test_system_shm_register/register HTTP/1.1\r\n" - - self.send_chunked_request( - request_header, self._malicious_chunk_count, self._parse_error - ) - - def test_cuda_shm_register(self): - request_header = f"POST /v2/cudasharedmemory/region/test_cuda_shm_register/register HTTP/1.1\r\n" - - self.send_chunked_request( - request_header, self._malicious_chunk_count, self._parse_error + # Wait until RSS is stable across several measurements before continuing. + server = get_server_process_from_env("SERVER_PID") + wait_for_stable_rss(server) + + # Monitor RSS growth over 100 requests. + repeat_request_count = 100 + rss_before = server.memory_info().rss + max_rss_growth_bytes = 1 * MIB + + for _ in range(repeat_request_count): + self.send_chunked_request( + self._infer_chunked_header(), + many_chunks, # way over max chunks + self._over_max_chunks_error, + ) + + rss_after = server.memory_info().rss + growth = rss_after - rss_before + print( + f"RSS: before={rss_before / MIB:.1f} MiB, " + f"after={rss_after / MIB:.1f} MiB, " + f"growth={growth / MIB:.1f} MiB, " + f"limit={max_rss_growth_bytes / MIB:.0f} MiB", + flush=True, ) - - def test_generate(self): - request_header = f"POST /v2/models/{self._model_name}/generate HTTP/1.1\r\n" - self.send_chunked_request( - request_header, self._malicious_chunk_count, self._parse_error + self.assertLess( + growth, + max_rss_growth_bytes, + f"Server RSS grew by {growth / MIB:.1f} MiB after " + f"{repeat_request_count} over-limit chunked infer requests " + f"(limit {max_rss_growth_bytes / MIB:.0f} MiB).", ) diff --git a/qa/L0_http/test.sh b/qa/L0_http/test.sh index c66300b543..e6b48272d2 100755 --- a/qa/L0_http/test.sh +++ b/qa/L0_http/test.sh @@ -950,7 +950,7 @@ if [ "$SERVER_PID" == "0" ]; then fi set +e -python $REQUEST_MANY_CHUNKS_PY -v >> ${CLIENT_LOG} 2>&1 +SERVER_PID=$SERVER_PID python $REQUEST_MANY_CHUNKS_PY -v >> ${CLIENT_LOG} 2>&1 if [ $? -ne 0 ]; then echo -e "\n***\n*** HTTP Request Many Chunks Test Failed\n***" cat $SERVER_LOG diff --git a/qa/L0_sagemaker/sagemaker_request_many_chunks.py b/qa/L0_sagemaker/sagemaker_request_many_chunks.py index 415ddcba28..e7ef8dd686 100755 --- a/qa/L0_sagemaker/sagemaker_request_many_chunks.py +++ b/qa/L0_sagemaker/sagemaker_request_many_chunks.py @@ -1,5 +1,5 @@ #!/usr/bin/python -# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -26,19 +26,32 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import socket +import sys import unittest +sys.path.append("../common") +from test_util import MIB, get_server_process_from_env, wait_for_stable_rss + class SagemakerRequestManyChunksTest(unittest.TestCase): def setUp(self): self._local_host = "localhost" self._sagemaker_port = 8080 - self._malicious_chunk_count = ( - 1000000 # large enough to cause a stack overflow if using alloca() + # Must match server kMaxChunkedChunks (http_server.cc). + self._k_max_chunked_chunks = 65536 + self._over_max_chunks_error = f"Chunked request body exceeds maximum of {self._k_max_chunked_chunks} non-empty chunks. Send fewer or larger HTTP chunks." + + def _sagemaker_chunked_header(self): + return ( + f"POST /models HTTP/1.1\r\n" f"X-Amzn-SageMaker-Target-Model: ZZZZZZZ\r\n" ) def send_chunked_request( - self, header: str, chunk_count: int, expected_response: str + self, + header: str, + chunk_count: int, + expected_response: str, + expected_http_status=400, ): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) header = ( @@ -54,11 +67,18 @@ def send_chunked_request( # HTTP request with chunked encoding s.sendall((header.encode())) - # Send chunked payload + # Send chunked payload (server may close early when over chunk limit) for _ in range(chunk_count): - s.send(b"1\r\nA\r\n") - # End chunked encoding - s.sendall(b"0\r\n\r\n") + try: + s.send(b"1\r\nA\r\n") + except (BrokenPipeError, ConnectionResetError): + break + try: + s.sendall(b"0\r\n\r\n") + except (BrokenPipeError, ConnectionResetError): + # Server may close/reset early after detecting chunk-limit violation. + # In that case, failing to send the terminating chunk is expected. + pass # Receive response response = b"" @@ -68,24 +88,86 @@ def send_chunked_request( if not chunk: break response += chunk + except ConnectionResetError: + break except socket.timeout: break + self.assertTrue( + response, + "expected error response body, but socket closed/reset before any bytes", + ) + status_line = response.split(b"\r\n", 1)[0].decode(errors="replace") + self.assertTrue( + status_line.startswith(f"HTTP/1.1 {expected_http_status} "), + f"expected HTTP status {expected_http_status}, got {status_line!r}", + ) self.assertIn(expected_response, response.decode()) except Exception as e: raise (e) finally: s.close() - def test_load_model(self): - request_header = ( - f"POST /models HTTP/1.1\r\n" f"X-Amzn-SageMaker-Target-Model: ZZZZZZZ\r\n" - ) + def test_chunked_at_max_chunks(self): self.send_chunked_request( - request_header, - self._malicious_chunk_count, + self._sagemaker_chunked_header(), + self._k_max_chunked_chunks, "failed to parse the request JSON buffer: Invalid value. at 0", ) + def test_chunked_rejected_over_max_chunks(self): + self.send_chunked_request( + self._sagemaker_chunked_header(), + self._k_max_chunked_chunks + 1, + self._over_max_chunks_error, + ) + + def test_chunked_over_max_chunks_reject_with_bounded_rss_growth(self): + many_chunks = 1000000 + + # verify server is running + server = get_server_process_from_env("SERVER_PID") + self.assertTrue(server.is_running()) + + # warm up and wait until RSS is stable. + self.send_chunked_request( + self._sagemaker_chunked_header(), + 1000000, # way over max chunks + self._over_max_chunks_error, + ) + # Wait until RSS is stable across several measurements before continuing. + server = get_server_process_from_env("SERVER_PID") + wait_for_stable_rss(server) + + # Monitor RSS growth over 100 requests. + repeat_request_count = 100 + rss_before = server.memory_info().rss + # TODO: Why sagemaker server occasionally grows >10 MiB but http server always smaller than 1 MiB? + max_rss_growth_bytes = 20 * MIB + + for _ in range(repeat_request_count): + self.send_chunked_request( + self._sagemaker_chunked_header(), + many_chunks, # way over max chunks + self._over_max_chunks_error, + ) + + rss_after = server.memory_info().rss + growth = rss_after - rss_before + print( + f"RSS: before={rss_before / MIB:.1f} MiB, " + f"after={rss_after / MIB:.1f} MiB, " + f"growth={growth / MIB:.1f} MiB, " + f"limit={max_rss_growth_bytes / MIB:.0f} MiB", + flush=True, + ) + self.assertLess( + growth, + max_rss_growth_bytes, + f"Server RSS grew by {growth / MIB:.1f} MiB after " + f"{repeat_request_count} over-limit chunked infer requests " + f"(limit {max_rss_growth_bytes / MIB:.0f} MiB).", + ) + if __name__ == "__main__": unittest.main() diff --git a/qa/L0_sagemaker/test.sh b/qa/L0_sagemaker/test.sh index 7bdb33aeef..61ed1b3b48 100755 --- a/qa/L0_sagemaker/test.sh +++ b/qa/L0_sagemaker/test.sh @@ -327,7 +327,7 @@ fi # Helper library to parse SSE events # https://github.com/mpetazzoni/sseclient -pip install sseclient-py +pip install sseclient-py psutil # Inference with generate_stream inference type set +e @@ -608,7 +608,7 @@ if [ "$code" != "200" ]; then fi set +e -python $REQUEST_MANY_CHUNKS_PY >>$CLIENT_LOG 2>&1 +SERVER_PID=$SERVER_PID python $REQUEST_MANY_CHUNKS_PY >>$CLIENT_LOG 2>&1 if [ $? -ne 0 ]; then echo -e "\n***\n*** Sagemaker Request Many Chunks Test Failed\n***" cat $SERVER_LOG diff --git a/qa/common/test_util.py b/qa/common/test_util.py index 335c6d9145..326c2b55d3 100755 --- a/qa/common/test_util.py +++ b/qa/common/test_util.py @@ -27,6 +27,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import json +import os import unittest import numpy as np @@ -37,6 +38,45 @@ # We use this dummy dtype as a representative for BF16. np_dtype_bfloat16 = np.dtype([("bf16", object)]) +MIB = 2**20 # 1 MIB = 1,048,576 bytes +GIB = 2**30 # 1 GIB = 1,073,741,824 bytes + + +def get_server_process_from_env(env_var="SERVER_PID"): + """ + Return a psutil.Process for the tritonserver under test. + """ + import psutil + + pid_str = os.environ.get(env_var) + if not pid_str: + raise AssertionError(f"{env_var} env var is not set") + try: + return psutil.Process(int(pid_str)) + except (ValueError, psutil.NoSuchProcess) as e: + raise AssertionError(f"Invalid or stale {env_var}={pid_str!r}: {e}") + + +def wait_for_stable_rss(server, rss_tolerance_bytes=0.1 * MIB, stable_threshold=10): + """ + Wait until the RSS of the server is stable. + """ + import time + + last_rss = None + stable_count = 0 + while True: + rss = server.memory_info().rss + if last_rss is not None and abs(rss - last_rss) < rss_tolerance_bytes: + stable_count += 1 + else: + stable_count = 0 + last_rss = rss + if stable_count >= stable_threshold: + break + time.sleep(0.1) + return + def shape_element_count(shape): cnt = 0 diff --git a/src/http_server.cc b/src/http_server.cc index d083f4da9f..d351956449 100644 --- a/src/http_server.cc +++ b/src/http_server.cc @@ -31,9 +31,11 @@ #include "http_server.h" #include +#include #include #include +#include #include #include #include @@ -48,6 +50,11 @@ namespace triton { namespace server { +// Transfer-Encoding: chunked — count non-empty chunks per request (worker +// thread). Increment in ChunkCountIncrement. Caps evbuffer fan-out / RSS. +constexpr uint64_t kMaxChunkedChunks = + 1 << 16; // reject on chunk count > kMaxChunkedChunks (65536) + #define RETURN_AND_CALLBACK_IF_ERR(X, CALLBACK) \ do { \ TRITONSERVER_Error* err__ = (X); \ @@ -288,12 +295,47 @@ HTTPServer::NewConnection(evhtp_connection_t* conn, void* arg) } server->conn_cnt_++; } + evhtp_connection_set_hook( + conn, evhtp_hook_on_new_chunk, + (evhtp_hook)(void*)HTTPServer::ChunkCountIncrement, arg); evhtp_connection_set_hook( conn, evhtp_hook_on_connection_fini, (evhtp_hook)(void*)HTTPServer::EndConnection, arg); return EVHTP_RES_OK; } +evhtp_res +HTTPServer::ChunkCountIncrement( + evhtp_request_t* req, uint64_t chunk_len, void* arg) +{ + (void)arg; + if ((req->flags & EVHTP_REQ_FLAG_FINISHED) != 0) { + return EVHTP_RES_OK; + } + + if (chunk_len == 0) { + return EVHTP_RES_OK; + } + if (++req->chunk_count > kMaxChunkedChunks) { + AddContentTypeHeader(req, "application/json"); + const std::string msg = + std::string("Chunked request body exceeds maximum of ") + + std::to_string(kMaxChunkedChunks) + + " non-empty chunks. Send fewer or larger HTTP chunks."; + EVBufferAddErrorJson(req->buffer_out, msg.c_str()); + // Force connection close after flushing this error response. + req->flags &= ~EVHTP_REQ_FLAG_KEEPALIVE; + if ((req->conn != nullptr) && (req->conn->bev != nullptr)) { + // Stop ingesting request bytes once over limit so memory won't keep + // growing while we flush the error response. + bufferevent_disable(req->conn->bev, EV_READ); + } + evhtp_send_reply(req, EVHTP_RES_BADREQ); + return EVHTP_RES_OK; + } + return EVHTP_RES_OK; +} + evhtp_res HTTPServer::EndConnection(evhtp_connection_t* conn, void* arg) { diff --git a/src/http_server.h b/src/http_server.h index 2a5ce0ef4a..a3f4e6b93a 100644 --- a/src/http_server.h +++ b/src/http_server.h @@ -105,6 +105,10 @@ class HTTPServer { static void StopCallback(evutil_socket_t sock, short events, void* arg); + // Transfer-Encoding: chunked — increment count per non-empty chunk (capped). + static evhtp_res ChunkCountIncrement( + evhtp_request_t* req, uint64_t chunk_len, void* arg); + static evhtp_res NewConnection(evhtp_connection_t* conn, void* arg); static evhtp_res EndConnection(evhtp_connection_t* conn, void* arg); From c7a1312cc3e480f9b9ebbce0179f733215bc7ae3 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Wed, 13 May 2026 10:15:50 -0700 Subject: [PATCH 211/230] chore(version): Update development version 2.70.0 / 26.06 (#8777) --- TRITON_VERSION | 2 +- build.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/TRITON_VERSION b/TRITON_VERSION index 5c6f0d3953..6a166a54c5 100644 --- a/TRITON_VERSION +++ b/TRITON_VERSION @@ -1 +1 @@ -2.69.0dev +2.70.0dev diff --git a/build.py b/build.py index 557f55ae77..9baaedbfe3 100755 --- a/build.py +++ b/build.py @@ -71,8 +71,8 @@ # DEFAULT_TRITON_VERSION_MAP = { - "release_version": "2.69.0dev", - "triton_container_version": "26.05dev", + "release_version": "2.70.0dev", + "triton_container_version": "26.06dev", "upstream_container_version": "26.04", "ort_version": "1.24.4", "ort_openvino_version": "2026.1.0", From 5133f7ba17ae59ae34b232fcddd72dae9b26c1e9 Mon Sep 17 00:00:00 2001 From: J Wyman Date: Fri, 15 May 2026 15:13:04 -0400 Subject: [PATCH 212/230] test: Add Torch AOTI Tests (#8771) This change: Creates a new L0_torch_aoti test suit. Adds complex Torch AOTI model generation to qa/common/gen_qa_models.py. Cleans up existion AOTI model generation in qa/common/gen_qa_models.py. Enabled torchvision AOTI model generation in qa/common/gen_qa_model_repository. --- qa/L0_torch_aoti/test.sh | 148 +++++++ qa/L0_torch_aoti/torch_aoti_infer_test.py | 284 +++++++++++++ qa/common/gen_qa_model_repository | 2 +- qa/common/gen_qa_models.py | 492 +++++++++++++++++----- 4 files changed, 821 insertions(+), 105 deletions(-) create mode 100755 qa/L0_torch_aoti/test.sh create mode 100755 qa/L0_torch_aoti/torch_aoti_infer_test.py diff --git a/qa/L0_torch_aoti/test.sh b/qa/L0_torch_aoti/test.sh new file mode 100755 index 0000000000..f37751c55e --- /dev/null +++ b/qa/L0_torch_aoti/test.sh @@ -0,0 +1,148 @@ +#!/bin/bash +# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +source ../common/util.sh + +if [[ "${DEBUG}" == "true" ]]; then + set -x +else + set +x +fi + +COLOR_DARK="\033[90m" +COLOR_ERROR="\033[31m" +COLOR_INFO="\033[94m" +COLOR_RESET="\033[0m" +COLOR_STATUS="\033[36m" +COLOR_SUCCESS="\033[32m" +COLOR_WARNING="\033[33m" +RET=0 + +REPO_VERSION=${NVIDIA_TRITON_SERVER_VERSION} +if [[ "$#" -ge 1 ]]; then + REPO_VERSION=$1 +fi +if [[ -z "$REPO_VERSION" ]]; then + echo -e "${COLOR_ERROR}Repository version must be specified${COLOR_RESET}" 1>&2 + echo -e "${COLOR_ERROR}\n***\n*** Test Failed\n***${COLOR_RESET}" 1>&2 + exit 1 +fi +if [[ ! -z "$TEST_REPO_ARCH" ]]; then + REPO_VERSION=${REPO_VERSION}_${TEST_REPO_ARCH} +fi + +export CUDA_VISIBLE_DEVICES=0 + +MODELDIR=${MODELDIR:=`pwd`/models} +DATADIR=${DATADIR:="/data/inferenceserver/${REPO_VERSION}"} +TRITON_DIR=${TRITON_DIR:="/opt/tritonserver"} +SERVER=${TRITON_DIR}/bin/tritonserver +BACKEND_DIR=${TRITON_DIR}/backends + +# PyTorch on SBSA requires libgomp to be loaded first. See the following +# GitHub issue for more information: +# https://github.com/pytorch/pytorch/issues/2575 +arch=`uname -m` +echo -e "${COLOR_DARK}Detected architecture: ${arch}${COLOR_RESET}" +if [[ "${arch}" == "aarch64" ]]; then + SERVER_LD_PRELOAD=/usr/lib/$(uname -m)-linux-gnu/libgomp.so.1 + echo -e "${COLOR_DARK}SERVER_LD_PRELOAD=${SERVER_LD_PRELOAD}${COLOR_RESET}" +fi + +# If BACKENDS not specified, set to all +BACKENDS=${BACKENDS:="pytorch"} +export BACKENDS + +# Copy the models into the model repository +echo -e "${COLOR_DARK}Setting up model repository in ${MODELDIR}${COLOR_RESET}" +rm -rf ${MODELDIR} && mkdir -p ${MODELDIR} +models=( + "torch_aoti_complex_index" + "torch_aoti_complex_named" + "torch_aoti_int8_int8" + "torch_aoti_int16_int16" + "torch_aoti_int32_int32" + "torch_aoti_int64_int64" + "torch_aoti_float16_float16" + "torch_aoti_float32_float32" + "torchvision_aoti" +) +for model in "${models[@]}"; do + cp -r ${DATADIR}/qa_model_repository/${model} ${MODELDIR}/${model} + echo -e "${COLOR_DARK}ls ${MODELDIR}/${model}${COLOR_RESET}" + ls -lha ${MODELDIR}/${model} +done +echo -e "${COLOR_DARK}ls ${MODELDIR}${COLOR_RESET}" +ls -lha ${MODELDIR} + +SERVER_ARGS="--model-repository=${MODELDIR} --log-verbose=1" +SERVER_LOG="./torch_aoti_complex_named-server.log" +CLIENT_LOG="./torch_aoti_complex_named-client.log" + +echo -e "${COLOR_DARK}Running ${SERVER} with model repository ${MODELDIR}${COLOR_RESET}" +run_server +if [[ "${SERVER_PID}" -eq 0 ]]; then + echo -e "${COLOR_ERROR}\n***\n*** Failed to start ${SERVER}\n***${COLOR_RESET}" &1>2 + cat ${SERVER_LOG} &1>2 + echo -e "\n" &1>2 + exit 1 +fi + +# Install torch framework +echo -e "${COLOR_DARK}Installing PyTorch framework required by tests${COLOR_RESET}" +pip install torch + +# Run the Tests +TEST_NAME="torch_aoti_infer_test" +python3 ./${TEST_NAME}.py >> ${CLIENT_LOG} 2>&1 +EXIT_CODE=$? +if [[ ${EXIT_CODE} -ne 0 ]]; then + echo -e "${COLOR_ERROR}\n***\n*** Test '${TEST_NAME}' Failed with exit code ${EXIT_CODE}\n***${COLOR_RESET}" &1>2 + cat ${CLIENT_LOG} &1>2 + echo -e "\n" &1>2 + RET=1 +else + echo -e "${COLOR_INFO}\n***\n*** Test '${TEST_NAME}' Passed\n***${COLOR_RESET}" +fi + +# Cleanup +echo -e "${COLOR_DARK}Killing server (pid: ${SERVER_PID})${COLOR_RESET}" +kill -s SIGINT ${SERVER_PID} +wait ${SERVER_PID} || true +echo -e "${COLOR_DARK}Removing model repository${COLOR_RESET}" +for model in "${models[@]}"; do + rm -rf ${MODELDIR}/${model} +done + +# Report results and exit. +if [[ ${RET} -ne 0 ]]; then + echo -e "${COLOR_ERROR}\n***\n*** Test Suite FAILED\n***${COLOR_RESET}" &1>2 +else + echo -e "${COLOR_SUCCESS}\n***\n*** Test Suite PASSED\n***${COLOR_RESET}" +fi + +exit ${RET} diff --git a/qa/L0_torch_aoti/torch_aoti_infer_test.py b/qa/L0_torch_aoti/torch_aoti_infer_test.py new file mode 100755 index 0000000000..2b93f31a48 --- /dev/null +++ b/qa/L0_torch_aoti/torch_aoti_infer_test.py @@ -0,0 +1,284 @@ +#!/usr/bin/python +# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import sys + +sys.path.append("../common") + +import unittest + +import test_util as tu +import torch +import tritonclient.http as http + + +class TorchAotiTest(tu.TestResultCollector): + def _get_complex_input_shape(self): + return (1, 16) + + def _get_complex_output_shape(self): + return (1, 16) + + def _get_complex_input_data(self, shape): + return [ + torch.randint(low=0, high=127, size=shape, dtype=torch.int8).numpy(), + torch.randint(low=0, high=127, size=shape, dtype=torch.int8).numpy(), + torch.randint(low=0, high=127, size=shape, dtype=torch.int8).numpy(), + torch.randint(low=0, high=127, size=shape, dtype=torch.int8).numpy(), + ] + + def _get_simple_input_data(self, shape, io_type): + if io_type in [torch.int8, torch.int16, torch.int32, torch.int64]: + return torch.randint(low=0, high=127, size=shape, dtype=io_type).numpy() + elif io_type in [torch.float16, torch.float32, torch.float64]: + return torch.randn(size=shape, dtype=io_type).numpy() + else: + raise ValueError(f"Unsupported data type: {io_type}") + + def _get_torchvision_input_data(self, shape): + return torch.randn(size=shape, dtype=torch.float32).numpy() + + def _dtype_to_triton_dtype(self, dtype): + if dtype == torch.int8: + return "INT8" + elif dtype == torch.int16: + return "INT16" + elif dtype == torch.int32: + return "INT32" + elif dtype == torch.int64: + return "INT64" + elif dtype == torch.float16: + return "FP16" + elif dtype == torch.float32: + return "FP32" + else: + raise ValueError(f"Unsupported data type: {dtype}") + + def _get_simple_model_name(self, io_type): + if io_type == torch.int8: + return "torch_aoti_int8_int8" + elif io_type == torch.int16: + return "torch_aoti_int16_int16" + elif io_type == torch.int32: + return "torch_aoti_int32_int32" + elif io_type == torch.int64: + return "torch_aoti_int64_int64" + elif io_type == torch.float16: + return "torch_aoti_float16_float16" + elif io_type == torch.float32: + return "torch_aoti_float32_float32" + else: + raise ValueError(f"Unsupported data type: {io_type}") + + def test_complex_index(self): + MODEL_NAME = "torch_aoti_complex_index" + INPUT_SHAPE = self._get_complex_input_shape() + OUTPUT_SHAPE = self._get_complex_output_shape() + + input_data = self._get_complex_input_data(INPUT_SHAPE) + + with http.InferenceServerClient("localhost:8000") as client: + inputs = [ + http.InferInput("INPUT__0", input_data[0].shape, "INT8"), + http.InferInput("INPUT__1", input_data[1].shape, "INT8"), + http.InferInput("INPUT__2", input_data[2].shape, "INT8"), + http.InferInput("INPUT__3", input_data[3].shape, "INT8"), + ] + + inputs[0].set_data_from_numpy(input_data[0], binary_data=True) + inputs[1].set_data_from_numpy(input_data[1], binary_data=True) + inputs[2].set_data_from_numpy(input_data[2], binary_data=True) + inputs[3].set_data_from_numpy(input_data[3], binary_data=True) + + output_names = [ + "OUTPUT__0", + "OUTPUT__1", + "OUTPUT__2", + "OUTPUT__3", + "OUTPUT__4", + "OUTPUT__5", + ] + + outputs = [] + for output_name in output_names: + outputs.append(http.InferRequestedOutput(output_name, binary_data=True)) + + output_data = [] + results = client.infer(MODEL_NAME, inputs, outputs=outputs) + + for output_name in output_names: + output_data.append(results.as_numpy(output_name)) + + self.assertEqual(len(outputs), len(output_data)) + for data in output_data: + self.assertEqual(data.shape, OUTPUT_SHAPE) + + self.assertTrue((output_data[0] == (input_data[0] + input_data[1])).all()) + self.assertTrue((output_data[1] == input_data[0] - input_data[1]).all()) + self.assertTrue((output_data[2] == input_data[0]).all()) + self.assertTrue((output_data[3] == input_data[1]).all()) + self.assertTrue((output_data[4] == input_data[2]).all()) + self.assertTrue((output_data[5] == input_data[3]).all()) + + def test_complex_named(self): + MODEL_NAME = "torch_aoti_complex_named" + INPUT_SHAPE = self._get_complex_input_shape() + OUTPUT_SHAPE = self._get_complex_output_shape() + + input_data = self._get_complex_input_data(INPUT_SHAPE) + + with http.InferenceServerClient("localhost:8000") as client: + inputs = [ + http.InferInput("ARGS[0]", input_data[0].shape, "INT8"), + http.InferInput("ARGS[1]", input_data[1].shape, "INT8"), + http.InferInput("ARGS[2][option1]", input_data[2].shape, "INT8"), + http.InferInput("ARGS[2][option2]", input_data[3].shape, "INT8"), + ] + + inputs[0].set_data_from_numpy(input_data[0], binary_data=True) + inputs[1].set_data_from_numpy(input_data[1], binary_data=True) + inputs[2].set_data_from_numpy(input_data[2], binary_data=True) + inputs[3].set_data_from_numpy(input_data[3], binary_data=True) + + output_names = [ + "RESULT[AAA]", + "RESULT[BBB][0]", + "RESULT[BBB][1]", + "RESULT[CCC][option1]", + "RESULT[CCC][option2]", + "RESULT[ZZZ]", + ] + + outputs = [] + for output_name in output_names: + outputs.append(http.InferRequestedOutput(output_name, binary_data=True)) + + output_data = [] + results = client.infer(MODEL_NAME, inputs, outputs=outputs) + + for output_name in output_names: + output_data.append(results.as_numpy(output_name)) + + self.assertEqual(len(outputs), len(output_data)) + for data in output_data: + self.assertEqual(data.shape, OUTPUT_SHAPE) + + self.assertTrue((output_data[0] == (input_data[0] + input_data[1])).all()) + self.assertTrue((output_data[1] == input_data[0]).all()) + self.assertTrue((output_data[2] == input_data[1]).all()) + self.assertTrue((output_data[3] == input_data[2]).all()) + self.assertTrue((output_data[4] == input_data[3]).all()) + self.assertTrue((output_data[5] == (input_data[0] - input_data[1])).all()) + + def test_simple_model(self): + io_types = [ + torch.int8, + torch.int16, + torch.int32, + torch.int64, + torch.float16, + torch.float32, + ] + for io_type in io_types: + MODEL_NAME = self._get_simple_model_name(io_type) + INPUT_SHAPE = (16,) + OUTPUT_SHAPE = (16,) + TRITON_IO_TYPE = self._dtype_to_triton_dtype(io_type) + + input_data = ( + self._get_simple_input_data(INPUT_SHAPE, io_type), + self._get_simple_input_data(INPUT_SHAPE, io_type), + ) + + with http.InferenceServerClient("localhost:8000") as client: + inputs = [ + http.InferInput("ARGS[0]", input_data[0].shape, TRITON_IO_TYPE), + http.InferInput("ARGS[1]", input_data[1].shape, TRITON_IO_TYPE), + ] + + inputs[0].set_data_from_numpy(input_data[0], binary_data=True) + inputs[1].set_data_from_numpy(input_data[1], binary_data=True) + + output_names = [ + "RESULT", + ] + + outputs = [] + for output_name in output_names: + outputs.append( + http.InferRequestedOutput(output_name, binary_data=True) + ) + + output_data = [] + results = client.infer(MODEL_NAME, inputs, outputs=outputs) + + for output_name in output_names: + output_data.append(results.as_numpy(output_name)) + + self.assertEqual(len(outputs), len(output_data)) + for data in output_data: + self.assertEqual(data.shape, OUTPUT_SHAPE) + self.assertTrue((data == input_data[0] + input_data[1]).all()) + + def test_torchvision(self): + MODEL_NAME = "torchvision_aoti" + INPUT_SHAPE = (1, 3, 224, 224) + OUTPUT_SHAPE = (1, 1000) + + input_data = self._get_torchvision_input_data(INPUT_SHAPE) + input_data[0][0] = 1.0 + + with http.InferenceServerClient("localhost:8000") as client: + inputs = [ + http.InferInput("ARGS[0]", input_data.shape, "FP32"), + ] + + inputs[0].set_data_from_numpy(input_data, binary_data=True) + + output_names = [ + "RESULT", + ] + + outputs = [] + for output_name in output_names: + outputs.append(http.InferRequestedOutput(output_name, binary_data=True)) + + output_data = [] + results = client.infer(MODEL_NAME, inputs, outputs=outputs) + + for output_name in output_names: + output_data.append(results.as_numpy(output_name)) + + self.assertEqual(len(outputs), len(output_data)) + for data in output_data: + self.assertEqual(data.shape, OUTPUT_SHAPE) + output_tensor = torch.from_numpy(data) + self.assertTrue(torch.isfinite(output_tensor).all().item()) + + +if __name__ == "__main__": + unittest.main() diff --git a/qa/common/gen_qa_model_repository b/qa/common/gen_qa_model_repository index 328f42bbe0..d6ed5738f8 100755 --- a/qa/common/gen_qa_model_repository +++ b/qa/common/gen_qa_model_repository @@ -263,9 +263,9 @@ set -e PATH=$PATH:/usr/local/cuda-13.0/bin python3 $TRITON_MDLS_SRC_DIR/gen_qa_models.py --libtorch --models_dir=$TRITON_MDLS_QA_MODEL python3 $TRITON_MDLS_SRC_DIR/gen_qa_models.py --torch-aoti --models_dir=$TRITON_MDLS_QA_MODEL +python3 $TRITON_MDLS_SRC_DIR/gen_qa_models.py --torchvision-aoti --models_dir=$TRITON_MDLS_QA_MODEL chmod -R 777 $TRITON_MDLS_QA_MODEL python3 $TRITON_MDLS_SRC_DIR/gen_qa_models.py --libtorch --variable --models_dir=$TRITON_MDLS_QA_VARIABLE_MODEL -python3 $TRITON_MDLS_SRC_DIR/gen_qa_models.py --torch-aoti --variable --models_dir=$TRITON_MDLS_QA_VARIABLE_MODEL chmod -R 777 $TRITON_MDLS_QA_VARIABLE_MODEL python3 $TRITON_MDLS_SRC_DIR/gen_qa_identity_models.py --libtorch --models_dir=$TRITON_MDLS_QA_IDENTITY_MODEL chmod -R 777 $TRITON_MDLS_QA_IDENTITY_MODEL diff --git a/qa/common/gen_qa_models.py b/qa/common/gen_qa_models.py index d509562bff..a015dc0e2d 100755 --- a/qa/common/gen_qa_models.py +++ b/qa/common/gen_qa_models.py @@ -47,6 +47,7 @@ from typing import List, Tuple _color_blue = "\033[94m" +_color_cyan = "\033[36m" _color_green = "\033[32m" _color_magenta = "\033[35m" _color_red = "\033[31m" @@ -1289,7 +1290,7 @@ def forward(self, INPUT0, INPUT1): traced.save(f"{model_version_dir}/model.pt") -def generate_sample_inputs( +def generate_torch_aoti_sample_inputs( input_shape, input_dtype, device, @@ -1297,70 +1298,32 @@ def generate_sample_inputs( # handle for -1 (when variable) since can't create tensor with shape of [-1] input_shape = [abs(ips) for ips in input_shape] - if input_dtype == np.int8: - input0 = torch.randint(-128, 127, input_shape, dtype=torch.int8, device=device) - input1 = torch.randint(-128, 127, input_shape, dtype=torch.int8, device=device) - elif input_dtype == np.int16: - input0 = torch.randint( - -32768, 32767, input_shape, dtype=torch.int16, device=device - ) - input1 = torch.randint( - -32768, 32767, input_shape, dtype=torch.int16, device=device - ) - elif input_dtype == np.int32: - input0 = torch.randint( - -2147483648, 2147483647, input_shape, dtype=torch.int32, device=device - ) - input1 = torch.randint( - -2147483648, 2147483647, input_shape, dtype=torch.int32, device=device - ) - elif input_dtype == np.int64: - input0 = torch.randint( - -9223372036854775808, - 9223372036854775807, - input_shape, - dtype=torch.int64, - device=device, - ) - input1 = torch.randint( - -9223372036854775808, - 9223372036854775807, - input_shape, - dtype=torch.int64, - device=device, - ) - elif input_dtype == np.float16: - input0 = torch.randn(*input_shape, dtype=torch.float16, device=device) - input1 = torch.randn(*input_shape, dtype=torch.float16, device=device) - elif input_dtype == np.float32: - input0 = torch.randn(*input_shape, dtype=torch.float32, device=device) - input1 = torch.randn(*input_shape, dtype=torch.float32, device=device) - elif input_dtype == np.float64: - input0 = torch.randn(*input_shape, dtype=torch.float64, device=device) - input1 = torch.randn(*input_shape, dtype=torch.float64, device=device) - elif input_dtype == np.uint8: - input0 = torch.randint(0, 255, input_shape, dtype=torch.uint8, device=device) - input1 = torch.randint(0, 255, input_shape, dtype=torch.uint8, device=device) - elif input_dtype == np.uint16: - input0 = torch.randint(0, 65535, input_shape, dtype=torch.uint16, device=device) - input1 = torch.randint(0, 65535, input_shape, dtype=torch.uint16, device=device) - elif input_dtype == np.uint32: - input0 = torch.randint( - 0, 4294967295, input_shape, dtype=torch.uint32, device=device - ) - input1 = torch.randint( - 0, 4294967295, input_shape, dtype=torch.uint32, device=device - ) - elif input_dtype == np.uint64: - input0 = torch.randint( - 0, 18446744073709551615, input_shape, dtype=torch.uint64, device=device - ) - input1 = torch.randint( - 0, 18446744073709551615, input_shape, dtype=torch.uint64, device=device + np_to_torch_dtype = { + np.int8: torch.int8, + np.int16: torch.int16, + np.int32: torch.int32, + np.int64: torch.int64, + np.float16: torch.float16, + np.float32: torch.float32, + np.float64: torch.float64, + np.uint8: torch.uint8, + np.uint16: torch.uint16, + np.uint32: torch.uint32, + np.uint64: torch.uint64, + } + + if input_dtype not in np_to_torch_dtype: + print( + f"{_color_yellow}warning: dtype {input_dtype} is unsupported; falling back to torch.int32{_color_reset}" ) - else: - input0 = torch.randn(*input_shape, device=device) - input1 = torch.randn(*input_shape, device=device) + input_dtype = np.int32 + + input0 = torch.zeros( + input_shape, dtype=np_to_torch_dtype[input_dtype], device=device + ) + input1 = torch.zeros( + input_shape, dtype=np_to_torch_dtype[input_dtype], device=device + ) return (input0, input1) @@ -1395,7 +1358,7 @@ def np_to_dtype(np_dtype): return torch.int32 -def create_torch_aoti_modelfile( +def create_torch_aoti_model_file( models_dir, model_version, input_shape, @@ -1418,7 +1381,7 @@ def create_torch_aoti_modelfile( ) return False - model_version_dir = f"{models_dir}/{model_name}/{model_version}" + model_version_dir = os.path.join(models_dir, model_name, str(model_version)) print(f"{_color_green}Creating model {model_name}{_color_reset}") @@ -1465,13 +1428,14 @@ def forward(self, INPUT0: torch.Tensor, INPUT1: torch.Tensor) -> torch.Tensor: model.to(device) model = model.eval() - sample_input = generate_sample_inputs(input_shape, input_dtype, device) + sample_inputs = generate_torch_aoti_sample_inputs(input_shape, input_dtype, device) + package_path = os.path.join(model_version_dir, "model.pt2") try: - ep = torch.export.export(model, sample_input) + exported_model = torch.export.export(model, sample_inputs) torch._inductor.aoti_compile_and_package( - ep, - package_path=f"{model_version_dir}/model.pt2", + exported_model, + package_path=package_path, ) except Exception as e: print( @@ -1484,13 +1448,162 @@ def forward(self, INPUT0: torch.Tensor, INPUT1: torch.Tensor) -> torch.Tensor: return True -def create_torchvision_aoti_modelfile( +def create_torch_aoti_complex_model_file( + models_dir: str, +): + base_name = "torch_aoti_complex" + model_names = [ + f"{base_name}_named", + f"{base_name}_index", + ] + model_version_dirs = [ + os.path.join(models_dir, model_names[0], "1"), + os.path.join(models_dir, model_names[1], "1"), + ] + + for model_version_dir in model_version_dirs: + try: + os.makedirs(model_version_dir) + except OSError: + pass # ignore existing dir + + print(f"{_color_green}Creating model {base_name}{_color_reset}") + + class TorchAotiComplex(torch.nn.Module): + def __init__(self): + super().__init__() + + def forward( + self, + hdata: torch.Tensor, + vdata: torch.Tensor, + options: dict[str, torch.Tensor], + ) -> dict[ + str, + torch.Tensor | tuple[torch.Tensor, torch.Tensor] | dict[str, torch.Tensor], + ]: + out = { + "AAA": hdata + vdata, + "ZZZ": hdata - vdata, + "BBB": ( + hdata, + vdata, + ), + "CCC": options, + } + + return out + + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + model = TorchAotiComplex() + model.to(device) + model = model.eval() + + SHAPE = (1, 16) + + sample_args = ( + torch.zeros(SHAPE, dtype=torch.int8, device=device), + torch.zeros(SHAPE, dtype=torch.int8, device=device), + { + "option1": torch.zeros(SHAPE, dtype=torch.int8, device=device), + "option2": torch.zeros(SHAPE, dtype=torch.int8, device=device), + }, + ) + + # Export and package the model + print(f"{_color_green}Exporting and packaging the model...{_color_reset}") + + model_file_name = "model.pt2" + package_paths = [ + os.path.join(model_version_dirs[0], model_file_name), + os.path.join(model_version_dirs[1], model_file_name), + ] + + try: + exported_model = torch.export.export(model, sample_args) + torch._inductor.aoti_compile_and_package( + exported_model, + package_path=package_paths[0], + ) + except Exception as e: + print( + f"{_color_red}error: Failed to create model {base_name}{_color_reset}", + file=sys.stderr, + ) + print(f"\n{_color_red}{e}{_color_reset}\n", file=sys.stderr) + return False + + try: + # Now load and run the packaged model + print(f"{_color_cyan}Loading and running the packaged model...{_color_reset}") + + compiled_model = torch._inductor.aoti_load_package(package_paths[0]) + + print(f"{_color_cyan}Compiled model call spec:{_color_reset}") + + for elem in compiled_model.loader.get_call_spec(): + print(elem) + + print(f"{_color_cyan}Running the compiled model...{_color_reset}") + + with torch.inference_mode(): + hdata = torch.randint( + low=0, + high=127, + size=SHAPE, + dtype=torch.int8, + device=device, + ) + vdata = torch.randint( + low=0, + high=127, + size=SHAPE, + dtype=torch.int8, + device=device, + ) + options = { + "option1": torch.randint( + low=0, + high=127, + size=SHAPE, + dtype=torch.int8, + device=device, + ), + "option2": torch.randint( + low=0, + high=127, + size=SHAPE, + dtype=torch.int8, + device=device, + ), + } + + _ = compiled_model(hdata, vdata, options) + + print( + f'{_color_green}Model "{base_name}" successfully executed.{_color_reset}' + ) + except Exception as e: + print( + f"{_color_red}error: Failed to validate model {base_name}{_color_reset}", + file=sys.stderr, + ) + print(f"\n{_color_red}{e}{_color_reset}\n", file=sys.stderr) + return False + + # Copy the compiled model package to the alternate model folder. + # Both the named and ordinal addressing versions of the model (from Triton's point-of-view) use the same compiled model. + shutil.copy(package_paths[0], package_paths[1]) + + return True + + +def create_torchvision_aoti_model_file( models_dir: str, max_batch: int, - model_version: int, ): model_name = "torchvision_aoti" - model_version_dir = f"{models_dir}/{model_name}/{model_version}" + model_version_dir = os.path.join(models_dir, model_name, "1") try: os.makedirs(model_version_dir) @@ -1504,16 +1617,16 @@ def create_torchvision_aoti_modelfile( model = model.to(device) model = model.eval() + SHAPE = (max_batch, 3, 224, 224) + # Example input tensor with batch size 1 and 3 color channels (RGB), height and width of 224 - input_tensor = torch.randn(max_batch, 3, 224, 224, device=device) + sample_inputs = (torch.zeros(SHAPE, dtype=torch.float32, device=device),) - try: - ep = torch.export.export(model, (input_tensor,)) + package_path = os.path.join(model_version_dir, "model.pt2") - torch._inductor.aoti_compile_and_package( - ep, - package_path=f"{model_version_dir}/model.pt2", - ) + try: + ep = torch.export.export(model, sample_inputs) + torch._inductor.aoti_compile_and_package(ep, package_path=package_path) except Exception as e: print( f"{_color_red}error: Failed to create model {model_name}{_color_reset}", @@ -1609,9 +1722,11 @@ def create_libtorch_modelconfig( except OSError: pass # ignore existing dir - with open(f"{config_dir}/config.pbtxt", "w") as file: + config_path = os.path.join(config_dir, "config.pbtxt") + + with open(config_path, "w") as file: file.write(config) - print(f"Created {config_dir}/config.pbtxt") + print(f"Created {config_path}") with open(f"{config_dir}/{label_filename}", "w") as file: for l in range(output0_label_cnt): @@ -1619,7 +1734,7 @@ def create_libtorch_modelconfig( print(f"Created {config_dir}/{label_filename}") -def create_torch_aoti_modelconfig( +def create_torch_aoti_model_config( models_dir, input_shape, output_shape, @@ -1650,7 +1765,7 @@ def create_torch_aoti_modelconfig( print(f"{_color_green}Creating config for {model_name}{_color_reset}") label_filename = "output_labels.txt" - config_dir = f"{models_dir}/{model_name}" + config_dir = os.path.join(models_dir, model_name) config = f""" backend: "pytorch" name: "{model_name}" @@ -1658,19 +1773,19 @@ def create_torch_aoti_modelconfig( version_policy: {version_policy_str} input [ {{ - name: "INPUT0" + name: "ARGS[0]" data_type: {np_to_model_dtype(input_dtype)} dims: [ {tu.shape_to_dims_str(input_shape)} ] }}, {{ - name: "INPUT1" + name: "ARGS[1]" data_type: {np_to_model_dtype(input_dtype)} dims: [ {tu.shape_to_dims_str(input_shape)} ] }} ] output [ {{ - name: "OUTPUT__0" + name: "RESULT" data_type: {np_to_model_dtype(output_dtype)} dims: [ {tu.shape_to_dims_str(output_shape)} ] label_filename: "{label_filename}" @@ -1684,17 +1799,173 @@ def create_torch_aoti_modelconfig( except OSError: pass # ignore existing dir - with open(f"{config_dir}/config.pbtxt", "w") as file: + config_path = os.path.join(config_dir, "config.pbtxt") + + with open(config_path, "w") as file: file.write(config) - print(f"Created {config_dir}/config.pbtxt") + print(f"Created {config_path}") - with open(f"{config_dir}/{label_filename}", "w") as file: + label_path = os.path.join(config_dir, label_filename) + + with open(label_path, "w") as file: for l in range(output_label_cnt): file.write(f"label{l}\n") - print(f"Created {config_dir}/{label_filename}") + print(f"Created {label_path}") + + +def create_torch_aoti_complex_model_config( + models_dir, +): + base_name = "torch_aoti_complex" + model_names = [ + f"{base_name}_named", + f"{base_name}_index", + ] + + print(f"{_color_green}Creating config for {base_name}{_color_reset}") + + config_dirs = [ + os.path.join(models_dir, model_names[0]), + os.path.join(models_dir, model_names[1]), + ] + configs = [ + f""" +backend: "pytorch" +platform: "torch_aoti" +name: "{model_names[0]}" +input: [ + {{ + name: "ARGS[0]" + data_type: TYPE_INT8 + dims: [1, 16] + }}, + {{ + name: "ARGS[1]" + data_type: TYPE_INT8 + dims: [1, 16] + }}, + {{ + name: "ARGS[2][option1]" + data_type: TYPE_INT8 + dims: [1, 16] + }}, + {{ + name: "ARGS[2][option2]" + data_type: TYPE_INT8 + dims: [1, 16] + }} +] +output: [ + {{ + name: "RESULT[AAA]" + data_type: TYPE_INT8 + dims: [1, 16] + }}, + {{ + name: "RESULT[BBB][0]" + data_type: TYPE_INT8 + dims: [1, 16] + }}, + {{ + name: "RESULT[BBB][1]" + data_type: TYPE_INT8 + dims: [1, 16] + }}, + {{ + name: "RESULT[CCC][option1]" + data_type: TYPE_INT8 + dims: [1, 16] + }}, + {{ + name: "RESULT[CCC][option2]" + data_type: TYPE_INT8 + dims: [1, 16] + }}, + {{ + name: "RESULT[ZZZ]" + data_type: TYPE_INT8 + dims: [1, 16] + }} +] +instance_group [{{ kind: {"KIND_GPU" if torch.cuda.is_available() else "KIND_CPU"} }}] +""", + f""" +backend: "pytorch" +name: "{model_names[1]}" +platform: "torch_aoti" +input: [ + {{ + name: "INPUT__0" + data_type: TYPE_INT8 + dims: [1, 16] + }}, + {{ + name: "INPUT__1" + data_type: TYPE_INT8 + dims: [1, 16] + }}, + {{ + name: "INPUT__2" + data_type: TYPE_INT8 + dims: [1, 16] + }}, + {{ + name: "INPUT__3" + data_type: TYPE_INT8 + dims: [1, 16] + }} +] +output: [ + {{ + name: "OUTPUT__0" + data_type: TYPE_INT8 + dims: [1, 16] + }}, + {{ + name: "OUTPUT__1" + data_type: TYPE_INT8 + dims: [1, 16] + }}, + {{ + name: "OUTPUT__2" + data_type: TYPE_INT8 + dims: [1, 16] + }}, + {{ + name: "OUTPUT__3" + data_type: TYPE_INT8 + dims: [1, 16] + }}, + {{ + name: "OUTPUT__4" + data_type: TYPE_INT8 + dims: [1, 16] + }}, + {{ + name: "OUTPUT__5" + data_type: TYPE_INT8 + dims: [1, 16] + }} +] +instance_group [{{ kind: {"KIND_GPU" if torch.cuda.is_available() else "KIND_CPU"} }}] +""", + ] + + for i in range(2): + config_dir = config_dirs[i] + try: + os.makedirs(config_dir) + except OSError: + pass # ignore existing dir + config_path = os.path.join(config_dir, "config.pbtxt") -def create_torchvision_aoti_modelconfig( + with open(config_path, "w") as file: + file.write(configs[i]) + print(f"Created {config_path}") + + +def create_torchvision_aoti_model_config( models_dir: str, max_batch: int, ): @@ -1703,7 +1974,7 @@ def create_torchvision_aoti_modelconfig( print(f"{_color_green}Creating config for {model_name}{_color_reset}") - config_dir = f"{models_dir}/{model_name}" + config_dir = os.path.join(models_dir, model_name) config = f""" backend: "pytorch" name: "{model_name}" @@ -1711,14 +1982,13 @@ def create_torchvision_aoti_modelconfig( max_batch_size: {max_batch} input [ {{ - name: "INPUT__0" + name: "ARGS[0]" data_type: TYPE_FP32 - format: FORMAT_NCHW dims: [ 3, 224, 224 ] }}] output [ {{ - name: "OUTPUT__0" + name: "RESULT" data_type: TYPE_FP32 dims: [ 1000 ] label_filename: "{label_filename}" @@ -1732,15 +2002,19 @@ def create_torchvision_aoti_modelconfig( except OSError: pass # ignore existing dir - with open(f"{config_dir}/config.pbtxt", "w") as file: + config_path = os.path.join(config_dir, "config.pbtxt") + + with open(config_path, "w") as file: file.write(config) - print(f"Created {config_dir}/config.pbtxt") + print(f"Created {config_path}") source_path = os.environ.get("TRITON_GENSRCDIR", default="gen_srcdir") source_filename = os.path.join(source_path, RESNET50_LABEL_FILE) - shutil.copyfile(source_filename, f"{config_dir}/{label_filename}") - print(f"Created {config_dir}/{label_filename}") + target_path = os.path.join(config_dir, label_filename) + + shutil.copyfile(source_filename, target_path) + print(f"Created {target_path}") def create_openvino_modelfile( @@ -2106,14 +2380,14 @@ def create_models( f"{_color_magenta}PyTorch: AOTI model generation requested{_color_reset}" ) # max-batch 8 - if create_torch_aoti_modelfile( + if create_torch_aoti_model_file( models_dir, model_version, input_shape, input_dtype, output0_dtype, ): - create_torch_aoti_modelconfig( + create_torch_aoti_model_config( models_dir, input_shape, output0_shape, @@ -2352,6 +2626,8 @@ def create_fixed_models( if FLAGS.onnx: import onnx if FLAGS.libtorch or FLAGS.torch_aoti: + import shutil + import torch from torch import nn if FLAGS.torchvision_aoti: @@ -2747,7 +3023,15 @@ def create_fixed_models( for model_shape in [(-1,), (-1, -1), (-1, -1, -1)]: emu.create_nop_modelconfig(FLAGS.models_dir, model_shape, model_dtype) + if FLAGS.torch_aoti: + print( + f"{_color_magenta}PyTorch: Complex AOTI model generation requested{_color_reset}" + ) + if create_torch_aoti_complex_model_file(FLAGS.models_dir): + create_torch_aoti_complex_model_config(FLAGS.models_dir) + if FLAGS.torchvision_aoti: + # TODO: Add support for variable batch size and version policy for torchvision AOTI models. print(f"{_color_blue}TorchVision AOTI model generation requested{_color_reset}") - if create_torchvision_aoti_modelfile(FLAGS.models_dir, 1, 1): - create_torchvision_aoti_modelconfig(FLAGS.models_dir, 1) + if create_torchvision_aoti_model_file(FLAGS.models_dir, 1): + create_torchvision_aoti_model_config(FLAGS.models_dir, 1) From 1e69d88b0fccd77c5782c316f6f32a7c2175622f Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Fri, 15 May 2026 13:02:33 -0700 Subject: [PATCH 213/230] fix(qa): ommit plugin creation if TensorRT branch is missed. (#8783) --- .../gen_qa_dyna_sequence_implicit_models.py | 3 +- qa/common/gen_qa_dyna_sequence_models.py | 6 ++- qa/common/gen_qa_identity_models.py | 6 ++- qa/common/gen_qa_implicit_models.py | 3 +- qa/common/gen_qa_model_repository | 54 +++++++------------ qa/common/gen_qa_models.py | 6 ++- qa/common/gen_qa_sequence_models.py | 6 ++- qa/common/gen_qa_trt_format_models.py | 3 +- qa/common/gen_qa_trt_plugin_models.py | 33 +++++++++--- 9 files changed, 67 insertions(+), 53 deletions(-) diff --git a/qa/common/gen_qa_dyna_sequence_implicit_models.py b/qa/common/gen_qa_dyna_sequence_implicit_models.py index 32e4ebea13..c69ca28eab 100755 --- a/qa/common/gen_qa_dyna_sequence_implicit_models.py +++ b/qa/common/gen_qa_dyna_sequence_implicit_models.py @@ -575,7 +575,8 @@ def create_plan_rf_modelfile(models_dir, model_version, max_batch, dtype, shape) flags = 1 << int(trt.BuilderFlag.DIRECT_IO) flags |= 1 << int(trt.BuilderFlag.PREFER_PRECISION_CONSTRAINTS) - flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) + if hasattr(trt.BuilderFlag, "REJECT_EMPTY_ALGORITHMS"): + flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) if trt_dtype == trt.int8: flags |= 1 << int(trt.BuilderFlag.INT8) diff --git a/qa/common/gen_qa_dyna_sequence_models.py b/qa/common/gen_qa_dyna_sequence_models.py index 7f8459b0da..1a26890f32 100755 --- a/qa/common/gen_qa_dyna_sequence_models.py +++ b/qa/common/gen_qa_dyna_sequence_models.py @@ -129,7 +129,8 @@ def create_plan_shape_tensor_modelfile( flags = 1 << int(trt.BuilderFlag.DIRECT_IO) flags |= 1 << int(trt.BuilderFlag.PREFER_PRECISION_CONSTRAINTS) - flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) + if hasattr(trt.BuilderFlag, "REJECT_EMPTY_ALGORITHMS"): + flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) if trt_dtype == trt.int8: flags |= 1 << int(trt.BuilderFlag.INT8) @@ -369,7 +370,8 @@ def create_plan_rf_modelfile(models_dir, model_version, max_batch, dtype, shape) flags = 1 << int(trt.BuilderFlag.DIRECT_IO) flags |= 1 << int(trt.BuilderFlag.PREFER_PRECISION_CONSTRAINTS) - flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) + if hasattr(trt.BuilderFlag, "REJECT_EMPTY_ALGORITHMS"): + flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) if trt_dtype == trt.int8: flags |= 1 << int(trt.BuilderFlag.INT8) diff --git a/qa/common/gen_qa_identity_models.py b/qa/common/gen_qa_identity_models.py index 248126bcc2..426d939d9e 100755 --- a/qa/common/gen_qa_identity_models.py +++ b/qa/common/gen_qa_identity_models.py @@ -584,7 +584,8 @@ def create_plan_dynamic_rf_modelfile( flags = 1 << int(trt.BuilderFlag.DIRECT_IO) flags |= 1 << int(trt.BuilderFlag.PREFER_PRECISION_CONSTRAINTS) - flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) + if hasattr(trt.BuilderFlag, "REJECT_EMPTY_ALGORITHMS"): + flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) datatype_set = set([trt_dtype]) for dt in datatype_set: if dt == trt.int8: @@ -707,7 +708,8 @@ def create_plan_shape_tensor_modelfile( flags = 1 << int(trt.BuilderFlag.DIRECT_IO) flags |= 1 << int(trt.BuilderFlag.PREFER_PRECISION_CONSTRAINTS) - flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) + if hasattr(trt.BuilderFlag, "REJECT_EMPTY_ALGORITHMS"): + flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) datatype_set = set([trt_dtype]) for dt in datatype_set: if dt == trt.int8: diff --git a/qa/common/gen_qa_implicit_models.py b/qa/common/gen_qa_implicit_models.py index 06b86ee5c2..c0800098ec 100755 --- a/qa/common/gen_qa_implicit_models.py +++ b/qa/common/gen_qa_implicit_models.py @@ -1066,7 +1066,8 @@ def create_plan_rf_modelfile(models_dir, model_version, max_batch, dtype, shape) flags = 1 << int(trt.BuilderFlag.DIRECT_IO) flags |= 1 << int(trt.BuilderFlag.PREFER_PRECISION_CONSTRAINTS) - flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) + if hasattr(trt.BuilderFlag, "REJECT_EMPTY_ALGORITHMS"): + flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) if trt_dtype == trt.int8: flags |= 1 << int(trt.BuilderFlag.INT8) diff --git a/qa/common/gen_qa_model_repository b/qa/common/gen_qa_model_repository index d6ed5738f8..dfb19e3344 100755 --- a/qa/common/gen_qa_model_repository +++ b/qa/common/gen_qa_model_repository @@ -352,43 +352,27 @@ python3 $TRITON_MDLS_SRC_DIR/gen_qa_trt_format_models.py --models_dir=$TRITON_MD chmod -R 777 $TRITON_MDLS_QA_TRT_FORMAT_MODEL nvidia-smi --query-gpu=compute_cap | grep -qzE '10\.7|11\.0' && echo -e '${COLOR_WARNING}[WARNING]${COLOR_RESET} Skipping model generation for data dependent shape (NonZero not supported on this GPU)${COLOR_RESET}' || python3 $TRITON_MDLS_SRC_DIR/gen_qa_trt_data_dependent_shape.py --models_dir=$TRITON_MDLS_QA_TRT_DATA_DEPENDENT_MODEL chmod -R 777 $TRITON_MDLS_QA_TRT_DATA_DEPENDENT_MODEL -# Make shared library for custom Hardmax plugin. -if [ -d "/usr/src/tensorrt/samples/python/onnx_custom_plugin" ]; then - cd /usr/src/tensorrt/samples/python/onnx_custom_plugin +# Build the custom Hardmax plugin used by L0_trt_plugin. The plugin source is +# pulled from the public NVIDIA/TensorRT repo at the release/ +# branch matching the runtime TRT version. If the branch is not published +# yet for this TRT version, the plugin model generation is skipped with a +# warning -- L0_trt_plugin will report missing artifacts but the rest of +# the QA model repository is still produced. +TRT_BRANCH=\$(echo \${TRT_VERSION} | cut -d . -f -2) +TRTSRC=/workspace/TensorRT +rm -rf \${TRTSRC} +if git clone --depth 1 -b release/\${TRT_BRANCH} \ + https://github.com/NVIDIA/TensorRT.git \${TRTSRC}; then + cd \${TRTSRC}/samples/python/onnx_custom_plugin && \ + rm -rf build && mkdir build && cd build && cmake .. && make -j && \ + cp libcustomHardmaxPlugin.so ${TRITON_MDLS_QA_TRT_PLUGIN_MODEL}/. + LD_PRELOAD=${TRITON_MDLS_QA_TRT_PLUGIN_MODEL}/libcustomHardmaxPlugin.so \ + python3 ${TRITON_MDLS_SRC_DIR}/gen_qa_trt_plugin_models.py \ + --models_dir=${TRITON_MDLS_QA_TRT_PLUGIN_MODEL} + chmod -R 777 ${TRITON_MDLS_QA_TRT_PLUGIN_MODEL} else - TRT_BRANCH=\$(echo \$TRT_VERSION | cut -d . -f -2) - if ! git clone -b release/\${TRT_BRANCH} --depth 1 https://github.com/NVIDIA/TensorRT.git /workspace/TensorRT; then - MAJOR=\$(echo "\$TRT_BRANCH" | cut -d . -f 1) - MINOR=\$(echo "\$TRT_BRANCH" | cut -d . -f 2) - if [ -n "\$MINOR" ] && [ "\$MINOR" -gt 0 ] 2>/dev/null; then - TRT_BRANCH="\${MAJOR}.\$((MINOR - 1))" - echo "Fallback: cloning TensorRT release/\${TRT_BRANCH} (previous minor)" - git clone -b release/\${TRT_BRANCH} --depth 1 https://github.com/NVIDIA/TensorRT.git /workspace/TensorRT - elif [ -n "\$MAJOR" ] && [ "\$MAJOR" -gt 0 ] 2>/dev/null; then - PREV_MAJOR=\$((MAJOR - 1)) - echo "Fallback: MINOR is 0, querying remote for latest release/\${PREV_MAJOR}.x branch" - TRT_BRANCH=\$(git ls-remote --heads https://github.com/NVIDIA/TensorRT.git "refs/heads/release/\${PREV_MAJOR}.*" \ - | awk -F'refs/heads/release/' '{print \$2}' \ - | awk -F. '{print \$2, \$0}' \ - | sort -k1,1n \ - | tail -1 \ - | awk '{print \$2}') - if [ -n "\$TRT_BRANCH" ]; then - echo "Fallback: cloning TensorRT release/\${TRT_BRANCH}" - git clone -b release/\${TRT_BRANCH} --depth 1 https://github.com/NVIDIA/TensorRT.git /workspace/TensorRT - else - exit 1 - fi - else - exit 1 - fi - fi - cd /workspace/TensorRT/samples/python/onnx_custom_plugin + echo "[WARNING] TensorRT release/\${TRT_BRANCH} not available on github.com/NVIDIA/TensorRT; skipping CustomHardmax plugin model generation. L0_trt_plugin coverage will be missing for this TRT version." fi -rm -rf build && mkdir build && \ -cd build && cmake .. && make -j && cp libcustomHardmaxPlugin.so $TRITON_MDLS_QA_TRT_PLUGIN_MODEL/. -LD_PRELOAD=$TRITON_MDLS_QA_TRT_PLUGIN_MODEL/libcustomHardmaxPlugin.so python3 $TRITON_MDLS_SRC_DIR/gen_qa_trt_plugin_models.py --models_dir=$TRITON_MDLS_QA_TRT_PLUGIN_MODEL -chmod -R 777 $TRITON_MDLS_QA_TRT_PLUGIN_MODEL exit 0 EOF diff --git a/qa/common/gen_qa_models.py b/qa/common/gen_qa_models.py index a015dc0e2d..cb8aa7a993 100755 --- a/qa/common/gen_qa_models.py +++ b/qa/common/gen_qa_models.py @@ -160,7 +160,8 @@ def create_plan_dynamic_rf_modelfile( profile.set_shape("INPUT1", min_shape, opt_shape, max_shape) flags = 1 << int(trt.BuilderFlag.PREFER_PRECISION_CONSTRAINTS) - flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) + if hasattr(trt.BuilderFlag, "REJECT_EMPTY_ALGORITHMS"): + flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) datatype_set = set([trt_input_dtype, trt_output0_dtype, trt_output1_dtype]) for dt in datatype_set: @@ -450,7 +451,8 @@ def create_plan_fixed_rf_modelfile( profile.set_shape("INPUT1", min_shape, opt_shape, max_shape) flags = 1 << int(trt.BuilderFlag.PREFER_PRECISION_CONSTRAINTS) - flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) + if hasattr(trt.BuilderFlag, "REJECT_EMPTY_ALGORITHMS"): + flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) datatype_set = set([trt_input_dtype, trt_output0_dtype, trt_output1_dtype]) for dt in datatype_set: diff --git a/qa/common/gen_qa_sequence_models.py b/qa/common/gen_qa_sequence_models.py index bf83a3a5f3..f8d89a5f9e 100755 --- a/qa/common/gen_qa_sequence_models.py +++ b/qa/common/gen_qa_sequence_models.py @@ -118,7 +118,8 @@ def create_plan_shape_tensor_modelfile( flags = 1 << int(trt.BuilderFlag.DIRECT_IO) flags |= 1 << int(trt.BuilderFlag.PREFER_PRECISION_CONSTRAINTS) - flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) + if hasattr(trt.BuilderFlag, "REJECT_EMPTY_ALGORITHMS"): + flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) if trt_dtype == trt.int8: flags |= 1 << int(trt.BuilderFlag.INT8) @@ -320,7 +321,8 @@ def create_plan_rf_modelfile(models_dir, model_version, max_batch, dtype, shape) flags = 1 << int(trt.BuilderFlag.DIRECT_IO) flags |= 1 << int(trt.BuilderFlag.PREFER_PRECISION_CONSTRAINTS) - flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) + if hasattr(trt.BuilderFlag, "REJECT_EMPTY_ALGORITHMS"): + flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) if trt_dtype == trt.int8: flags |= 1 << int(trt.BuilderFlag.INT8) diff --git a/qa/common/gen_qa_trt_format_models.py b/qa/common/gen_qa_trt_format_models.py index 5645a7178c..5f2cadd69e 100755 --- a/qa/common/gen_qa_trt_format_models.py +++ b/qa/common/gen_qa_trt_format_models.py @@ -147,7 +147,8 @@ def create_plan_modelfile( # The build will fail if TensorRT cannot build an engine without introducing such reformatting. The failure may happen only for some target platforms, because of what formats are supported by kernels for those platforms. # flags = 1 << int(trt.BuilderFlag.DIRECT_IO) flags = 1 << int(trt.BuilderFlag.PREFER_PRECISION_CONSTRAINTS) - flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) + if hasattr(trt.BuilderFlag, "REJECT_EMPTY_ALGORITHMS"): + flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) datatype_set = set([trt_input_dtype, trt_output0_dtype, trt_output1_dtype]) for dt in datatype_set: if dt == trt.int8: diff --git a/qa/common/gen_qa_trt_plugin_models.py b/qa/common/gen_qa_trt_plugin_models.py index 83ed9e82a7..9fd23d92a8 100755 --- a/qa/common/gen_qa_trt_plugin_models.py +++ b/qa/common/gen_qa_trt_plugin_models.py @@ -44,7 +44,12 @@ def get_trt_plugin(plugin_name): plugin = None field_collection = None - plugin_creators = trt.get_plugin_registry().plugin_creator_list + # The upstream onnx_custom_plugin sample is V2 on TRT 10.x release + # branches and V3 on rel-11.0 (and TRT 11 removed the V2 plugin + # registry surface). Pick the matching API at runtime. + registry = trt.get_plugin_registry() + use_v3 = not hasattr(registry, "plugin_creator_list") + plugin_creators = registry.all_creators if use_v3 else registry.plugin_creator_list for plugin_creator in plugin_creators: if (plugin_creator.name == "CustomHardmax") and ( plugin_name == "CustomHardmax" @@ -57,9 +62,16 @@ def get_trt_plugin(plugin_name): if field_collection is None: raise RuntimeError("Plugin not found: " + plugin_name) - plugin = plugin_creator.create_plugin( - name=plugin_name, field_collection=field_collection - ) + if use_v3: + plugin = plugin_creator.create_plugin( + name=plugin_name, + field_collection=field_collection, + phase=trt.TensorRTPhase.BUILD, + ) + else: + plugin = plugin_creator.create_plugin( + name=plugin_name, field_collection=field_collection + ) return plugin @@ -104,9 +116,16 @@ def create_plan_modelfile( input_layer = network.add_input( name="INPUT0", dtype=trt_input_dtype, shape=input_with_batchsize ) - plugin_layer = network.add_plugin_v2( - inputs=[input_layer], plugin=get_trt_plugin(plugin_name) - ) + # add_plugin_v2 was removed in TRT 11; add_plugin_v3 has existed since + # TRT 10.0. Pick the API that exists on this TRT install; the plugin + # object returned by get_trt_plugin() is matched to the same version. + plugin_obj = get_trt_plugin(plugin_name) + if hasattr(network, "add_plugin_v2"): + plugin_layer = network.add_plugin_v2(inputs=[input_layer], plugin=plugin_obj) + else: + plugin_layer = network.add_plugin_v3( + inputs=[input_layer], shape_inputs=[], plugin=plugin_obj + ) plugin_layer.get_output(0).name = "OUTPUT0" network.mark_output(plugin_layer.get_output(0)) From 665030a3a63d8eb5a185011ad0ea0d5277c574b4 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Mon, 18 May 2026 15:46:33 -0700 Subject: [PATCH 214/230] test: Align QA BF16 with ml_dtypes and generate ONNX BF16 models (#8782) --- qa/L0_backend_identity/identity_test.py | 51 +++------ .../gen_add_bf16_onnx_model.py | 100 ------------------ qa/L0_backend_onnxruntime/test.py | 65 +++++++----- qa/L0_backend_onnxruntime/test.sh | 33 +++--- qa/L0_backend_python/python_test.py | 13 +-- qa/L0_trt_bf16_dtype/trt_bf16_dtype_test.py | 10 +- qa/common/gen_common.py | 9 +- qa/common/gen_qa_model_repository | 7 +- qa/common/gen_qa_models.py | 65 +++++++----- qa/common/test_util.py | 5 +- 10 files changed, 132 insertions(+), 226 deletions(-) delete mode 100755 qa/L0_backend_onnxruntime/gen_add_bf16_onnx_model.py diff --git a/qa/L0_backend_identity/identity_test.py b/qa/L0_backend_identity/identity_test.py index a607e4189b..b96649241d 100755 --- a/qa/L0_backend_identity/identity_test.py +++ b/qa/L0_backend_identity/identity_test.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# Copyright 2019-2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -30,6 +30,7 @@ import sys from builtins import range +import ml_dtypes import numpy as np import requests as httpreq import tritonclient.grpc as grpcclient @@ -201,8 +202,8 @@ ("identity_nobatch_int8", np.int8, [0]), ("identity_nobatch_int8", np.int8, [7]), ("identity_bytes", object, [1, 1]), - ("identity_bf16", np.float32, [1, 0]), - ("identity_bf16", np.float32, [1, 5]) + ("identity_bf16", ml_dtypes.bfloat16, [1, 0]), + ("identity_bf16", ml_dtypes.bfloat16, [1, 5]) ): # yapf: enable if np_dtype != object: @@ -234,40 +235,20 @@ print("error: expected 'OUTPUT0'") sys.exit(1) - if model_name == "identity_bf16": - if input_data.shape != output_data.shape: - print( - "error: expected output shape {} to match input shape {}".format( - output_data.shape, input_data.shape - ) + if output_data.dtype != input_data.dtype: + print( + "error: expected output dtype {} to match input dtype {} for {}".format( + output_data.dtype, input_data.dtype, model_name ) - sys.exit(1) - for input, output in zip( - np.nditer(input_data, flags=["refs_ok", "zerosize_ok"], order="C"), - np.nditer(output_data, flags=["refs_ok", "zerosize_ok"], order="C"), - ): - if input.tobytes()[2:4] != output.tobytes()[2:4]: - print( - "error: expected low-order bits of output {} to match low-order bits of input {}".format( - output, input - ) - ) - sys.exit(1) - if output.tobytes()[0:2] != b"\x00\x00": - print( - "error: expected output {} to have all-zero high-order bits, got {}".format( - output, output.tobytes()[0:2] - ) - ) - sys.exit(1) - else: - if not np.array_equal(output_data, input_data): - print( - "error: expected output {} to match input {}".format( - output_data, input_data - ) + ) + sys.exit(1) + if not np.array_equal(output_data, input_data): + print( + "error: expected output {} to match input {} for {}".format( + output_data, input_data, model_name ) - sys.exit(1) + ) + sys.exit(1) # Make sure response parameters are correct response = results.get_response() diff --git a/qa/L0_backend_onnxruntime/gen_add_bf16_onnx_model.py b/qa/L0_backend_onnxruntime/gen_add_bf16_onnx_model.py deleted file mode 100755 index e3fc39b38e..0000000000 --- a/qa/L0_backend_onnxruntime/gen_add_bf16_onnx_model.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -# Generates the add_bf16 ONNX model and Triton config. -# Model: element-wise Add in BFLOAT16 (INPUT0 + INPUT1 = OUTPUT), ONNX Runtime backend. -import os - -import onnx - - -def generate_bf16_add_model(models_dir): - """Generate a simple BFLOAT16 Add model (INPUT0 + INPUT1 = OUTPUT).""" - model_name = "add_bf16" - shape = [1] - onnx_dtype = onnx.TensorProto.BFLOAT16 - - add = onnx.helper.make_node("Add", ["INPUT0", "INPUT1"], ["OUTPUT"]) - - input0 = onnx.helper.make_tensor_value_info("INPUT0", onnx_dtype, shape) - input1 = onnx.helper.make_tensor_value_info("INPUT1", onnx_dtype, shape) - output = onnx.helper.make_tensor_value_info("OUTPUT", onnx_dtype, shape) - - graph_proto = onnx.helper.make_graph( - [add], - model_name, - [input0, input1], - [output], - ) - model_def = onnx.helper.make_model(graph_proto, producer_name="triton") - # Cap IR version for older ONNX Runtime (e.g. max supported 11) - model_def.ir_version = min(model_def.ir_version, 11) - # BFLOAT16 support requires opset 13+ - model_def.opset_import[0].version = 13 - - model_dir = os.path.join(models_dir, model_name, "1") - os.makedirs(model_dir, exist_ok=True) - onnx.save(model_def, os.path.join(model_dir, "model.onnx")) - - # Write config.pbtxt - config = """platform: "onnxruntime_onnx" -max_batch_size: 0 -input [ - {{ - name: "INPUT0" - data_type: TYPE_BF16 - dims: {shape} - }}, - {{ - name: "INPUT1" - data_type: TYPE_BF16 - dims: {shape} - }} -] -output [ - {{ - name: "OUTPUT" - data_type: TYPE_BF16 - dims: {shape} - }} -] -""".format( - shape=shape - ) - - config_path = os.path.join(models_dir, model_name, "config.pbtxt") - with open(config_path, "w") as f: - f.write(config) - - print(f"Generated model '{model_name}' in {models_dir}") - - -if __name__ == "__main__": - models_dir = os.path.join(os.getcwd(), "models") - os.makedirs(models_dir, exist_ok=True) - generate_bf16_add_model(models_dir) diff --git a/qa/L0_backend_onnxruntime/test.py b/qa/L0_backend_onnxruntime/test.py index 1a5437cdce..7575f6d0f5 100755 --- a/qa/L0_backend_onnxruntime/test.py +++ b/qa/L0_backend_onnxruntime/test.py @@ -28,6 +28,7 @@ import os import unittest +import ml_dtypes import numpy as np import tritonclient.grpc as grpcclient import tritonclient.http as httpclient @@ -38,43 +39,51 @@ def setUp(self): self.protocol = os.environ.get("CLIENT_TYPE", "http") if self.protocol == "http": self.client_ = httpclient.InferenceServerClient("localhost:8000") + self.client_module_ = httpclient else: self.client_ = grpcclient.InferenceServerClient("localhost:8001") - self.model_name_ = "add_bf16" - self.shape_ = [1] + self.client_module_ = grpcclient + self.model_name_ = "onnx_bf16_bf16_bf16" + # Model dims are [-1, 16]: dynamic batch dim plus inner dim of 16. + self.inner_dim_ = 16 def _infer_bf16(self, input0_data, input1_data): - """Helper to run BF16 inference and return the output numpy array.""" - if self.protocol == "http": - input0 = httpclient.InferInput("INPUT0", self.shape_, "BF16") - input1 = httpclient.InferInput("INPUT1", self.shape_, "BF16") - else: - input0 = grpcclient.InferInput("INPUT0", self.shape_, "BF16") - input1 = grpcclient.InferInput("INPUT1", self.shape_, "BF16") + """Helper to run BF16 inference and return the output numpy arrays.""" + input0 = self.client_module_.InferInput( + "INPUT0", list(input0_data.shape), "BF16" + ) + input1 = self.client_module_.InferInput( + "INPUT1", list(input1_data.shape), "BF16" + ) input0.set_data_from_numpy(input0_data) input1.set_data_from_numpy(input1_data) results = self.client_.infer(self.model_name_, [input0, input1]) - return results.as_numpy("OUTPUT") + return results.as_numpy("OUTPUT0"), results.as_numpy("OUTPUT1") + + def test_bf16_add_sub_variants(self): + """Run BF16 add/sub across multiple cases batched in a single request: + zeros, negatives, large, small, cancellation, and identical.""" + cases = [ + (0.0, 0.0), + (-1.5, 3.5), + (100.0, 200.0), + (1e-2, 1e-2), + (1.0, -1.0), + (2.0, 2.0), + ] + batch_size = len(cases) + input0_data = np.empty((batch_size, self.inner_dim_), dtype=ml_dtypes.bfloat16) + input1_data = np.empty((batch_size, self.inner_dim_), dtype=ml_dtypes.bfloat16) + for i, (v0, v1) in enumerate(cases): + input0_data[i, :] = v0 + input1_data[i, :] = v1 - def test_bf16_add_variants(self): - """Run BF16 add across multiple cases: zeros, negatives, large, small, cancellation, and identical.""" - for input0_val, input1_val, expected_val in [ - (0.0, 0.0, 0.0), # zeros - (-1.5, 3.5, 2.0), # negatives / mixed - (100.0, 200.0, 300.0), # large - (1e-2, 1e-2, 2e-2), # small (near underflow) - (1.0, -1.0, 0.0), # cancellation - (2.0, 2.0, 4.0), # identical inputs - ]: - output = self._infer_bf16( - np.full(self.shape_, input0_val, dtype=np.float32), - np.full(self.shape_, input1_val, dtype=np.float32), - ) - self.assertEqual(output.dtype, np.float32) - # TODO: BF16 to FP32 conversion loses precision. Remove rtol and atol in TRI-801. - # BF16 has ~3 decimal digits; use relaxed tol for computed values - np.testing.assert_allclose(output, expected_val, rtol=1e-2, atol=1e-3) + output0, output1 = self._infer_bf16(input0_data, input1_data) + self.assertEqual(output0.dtype, ml_dtypes.bfloat16) + self.assertEqual(output1.dtype, ml_dtypes.bfloat16) + np.testing.assert_array_equal(output0, input0_data + input1_data) + np.testing.assert_array_equal(output1, input0_data - input1_data) if __name__ == "__main__": diff --git a/qa/L0_backend_onnxruntime/test.sh b/qa/L0_backend_onnxruntime/test.sh index f4e349c2be..2e66b467e9 100755 --- a/qa/L0_backend_onnxruntime/test.sh +++ b/qa/L0_backend_onnxruntime/test.sh @@ -25,8 +25,22 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +REPO_VERSION=${NVIDIA_TRITON_SERVER_VERSION} +if [ "$#" -ge 1 ]; then + REPO_VERSION=$1 +fi +if [ -z "$REPO_VERSION" ]; then + echo -e "Repository version must be specified" + echo -e "\n***\n*** Test Failed\n***" + exit 1 +fi +if [ ! -z "$TEST_REPO_ARCH" ]; then + REPO_VERSION=${REPO_VERSION}_${TEST_REPO_ARCH} +fi + export CUDA_VISIBLE_DEVICES=0 +DATADIR=/data/inferenceserver/${REPO_VERSION} SERVER=/opt/tritonserver/bin/tritonserver SERVER_LOG="./inference_server.log" CLIENT_LOG="./test.log" @@ -38,23 +52,8 @@ rm -rf models RET=0 # BFLOAT16 test -# Generate the model -mkdir -p models/add_bf16/1 -set +e - -pip install onnx==1.20.1 -if [ $? -ne 0 ]; then - echo -e "\n***\n*** Failed to install onnx dependency\n***" - exit 1 -fi - -python gen_add_bf16_onnx_model.py -if [ $? -ne 0 ]; then - echo -e "\n***\n*** Failed to generate BFLOAT16 ONNX model\n***" - exit 1 -fi - -set -e +mkdir -p models +cp -r ${DATADIR}/qa_model_repository/onnx_bf16_bf16_bf16 models/. SERVER_ARGS="--model-repository=`pwd`/models" run_server diff --git a/qa/L0_backend_python/python_test.py b/qa/L0_backend_python/python_test.py index c2512600e2..135aba3060 100755 --- a/qa/L0_backend_python/python_test.py +++ b/qa/L0_backend_python/python_test.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -33,6 +33,7 @@ import os import unittest +import ml_dtypes import numpy as np import requests as httpreq import shm_util @@ -372,9 +373,7 @@ def test_bf16(self): with httpclient.InferenceServerClient( f"{_tritonserver_ipaddr}:8000" ) as client: - # NOTE: Client will truncate FP32 to BF16 internally - # since numpy has no built-in BF16 representation. - np_input = np.ones(shape, dtype=np.float32) + np_input = np.ones(shape, dtype=ml_dtypes.bfloat16) inputs = [ httpclient.InferInput( "INPUT0", np_input.shape, "BF16" @@ -390,10 +389,8 @@ def test_bf16(self): np_output = result.as_numpy("OUTPUT0") self.assertIsNotNone(np_output) - # BF16 tensors are held in FP32 when converted to numpy due to - # lack of native BF16 support in numpy, so verify that. - self.assertEqual(np_output.dtype, np.float32) - self.assertTrue(np.allclose(np_output, np_input)) + self.assertEqual(np_output.dtype, np_input.dtype) + self.assertTrue(np.array_equal(np_output, np_input)) def test_infer_pytorch(self): # FIXME: This model requires torch. Because windows tests are not run in a docker diff --git a/qa/L0_trt_bf16_dtype/trt_bf16_dtype_test.py b/qa/L0_trt_bf16_dtype/trt_bf16_dtype_test.py index 265c1930b0..c80c84d353 100755 --- a/qa/L0_trt_bf16_dtype/trt_bf16_dtype_test.py +++ b/qa/L0_trt_bf16_dtype/trt_bf16_dtype_test.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -32,6 +32,7 @@ import unittest +import ml_dtypes import numpy as np import test_util as tu import tritonclient.http as client @@ -49,8 +50,9 @@ def _infer_helper(self, model_name, shape): inputs.append(client.InferInput("INPUT0", shape, "BF16")) inputs.append(client.InferInput("INPUT1", shape, "BF16")) - input0_data = np.ones(shape=shape).astype(np.float32) - input1_data = np.ones(shape=shape).astype(np.float32) + # BF16 inputs use ml_dtypes.bfloat16; numpy has no native BF16 dtype. + input0_data = np.ones(shape, dtype=ml_dtypes.bfloat16) + input1_data = np.ones(shape, dtype=ml_dtypes.bfloat16) inputs[0].set_data_from_numpy(input0_data, binary_data=True) inputs[1].set_data_from_numpy(input1_data, binary_data=True) @@ -63,6 +65,8 @@ def _infer_helper(self, model_name, shape): output0_data = results.as_numpy("OUTPUT0") output1_data = results.as_numpy("OUTPUT1") + self.assertEqual(output0_data.dtype, ml_dtypes.bfloat16) + self.assertEqual(output1_data.dtype, ml_dtypes.bfloat16) np.testing.assert_equal( output0_data, input0_data + input1_data, diff --git a/qa/common/gen_common.py b/qa/common/gen_common.py index d53702d604..db0869ef38 100644 --- a/qa/common/gen_common.py +++ b/qa/common/gen_common.py @@ -1,4 +1,4 @@ -# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -28,13 +28,14 @@ from typing import List # Common utilities for model generation scripts +import ml_dtypes import numpy as np np_dtype_string = np.dtype(object) # Numpy does not support the BF16 datatype natively. -# We use this dummy dtype as a representative for BF16. -np_dtype_bfloat16 = np.dtype([("bf16", object)]) +# We use ml_dtypes.bfloat16 for BF16. +np_dtype_bfloat16 = ml_dtypes.bfloat16 def np_to_onnx_dtype(np_dtype): @@ -62,6 +63,8 @@ def np_to_onnx_dtype(np_dtype): return onnx.TensorProto.DOUBLE elif np_dtype == np_dtype_string: return onnx.TensorProto.STRING + elif np_dtype == np_dtype_bfloat16: + return onnx.TensorProto.BFLOAT16 return None diff --git a/qa/common/gen_qa_model_repository b/qa/common/gen_qa_model_repository index dfb19e3344..8841060116 100755 --- a/qa/common/gen_qa_model_repository +++ b/qa/common/gen_qa_model_repository @@ -159,7 +159,7 @@ apt-get update && \ ln -s /usr/bin/python3 /usr/bin/python -pip3 install "numpy<=1.23.5" setuptools +pip3 install "numpy<=1.23.5" setuptools "ml_dtypes<=0.5.4" pip3 install openvino==$OPENVINO_VERSION @@ -202,7 +202,7 @@ apt-get update && \ protobuf-compiler python3 python3-dev python3-pip ln -s /usr/bin/python3 /usr/bin/python -pip3 install "protobuf<=3.20.1" "numpy<=1.23.5" # TODO: Remove current line DLIS-3838 +pip3 install "protobuf<=3.20.1" "numpy<=1.23.5" "ml_dtypes<=0.5.4" # TODO: Remove current line DLIS-3838 pip3 install --upgrade onnx==${ONNX_VERSION} python3 $TRITON_MDLS_SRC_DIR/gen_qa_models.py --onnx --onnx_opset=$ONNX_OPSET --models_dir=$TRITON_MDLS_QA_MODEL @@ -318,6 +318,9 @@ echo -e "${COLOR_INFO}Generating TensorRT models${COLOR_RESET}" set -e dpkg -l | grep TensorRT export TRT_SUPPRESS_DEPRECATION_WARNINGS=1 + +pip3 install "ml_dtypes<=0.5.4" + # Models using shape tensor i/o python3 $TRITON_MDLS_SRC_DIR/gen_qa_identity_models.py --tensorrt-shape-io --models_dir=$TRITON_MDLS_QA_SHAPETENSOR_MODEL python3 $TRITON_MDLS_SRC_DIR/gen_qa_sequence_models.py --tensorrt-shape-io --models_dir=$TRITON_MDLS_QA_SHAPETENSOR_MODEL diff --git a/qa/common/gen_qa_models.py b/qa/common/gen_qa_models.py index cb8aa7a993..d305f07ce3 100755 --- a/qa/common/gen_qa_models.py +++ b/qa/common/gen_qa_models.py @@ -2701,19 +2701,24 @@ def create_fixed_models( # Make multiple versions of some models for version testing # (they use different version policies when created above) - if FLAGS.tensorrt: - if tu.check_gpus_compute_capability(min_capability=8.0): - create_fixed_models( - FLAGS.models_dir, - np_dtype_bfloat16, - np_dtype_bfloat16, - np_dtype_bfloat16, - ) - else: - print( - "Skipping the generation of TensorRT PLAN models for the BF16 datatype!" - ) + # BF16 generation: TensorRT requires SM>=8.0; ONNX has no such + # requirement (model generation runs on CPU; ORT decides at runtime). + bf16_for_trt = FLAGS.tensorrt and tu.check_gpus_compute_capability( + min_capability=8.0 + ) + if FLAGS.tensorrt and not bf16_for_trt: + print( + "Skipping the generation of TensorRT PLAN models for the BF16 datatype!" + ) + if bf16_for_trt or FLAGS.onnx: + create_fixed_models( + FLAGS.models_dir, + np_dtype_bfloat16, + np_dtype_bfloat16, + np_dtype_bfloat16, + ) + if FLAGS.tensorrt: for vt in [np.float32, np.float16, np.int32, np.uint8]: create_plan_modelfile( FLAGS.models_dir, 8, 2, (16,), (16,), (16,), vt, vt, vt, swap=True @@ -2992,22 +2997,26 @@ def create_fixed_models( 32, ) - if FLAGS.tensorrt: - if tu.check_gpus_compute_capability(min_capability=8.0): - create_models( - FLAGS.models_dir, - np_dtype_bfloat16, - np_dtype_bfloat16, - np_dtype_bfloat16, - (-1, -1), - (-1, -1), - (-1, -1), - 0, - ) - else: - print( - "Skipping the generation of TensorRT PLAN models for the BF16 datatype!" - ) + # BF16 generation: TensorRT requires SM>=8.0; ONNX has no such + # requirement (model generation runs on CPU; ORT decides at runtime). + bf16_for_trt = FLAGS.tensorrt and tu.check_gpus_compute_capability( + min_capability=8.0 + ) + if FLAGS.tensorrt and not bf16_for_trt: + print( + "Skipping the generation of TensorRT PLAN models for the BF16 datatype!" + ) + if bf16_for_trt or FLAGS.onnx: + create_models( + FLAGS.models_dir, + np_dtype_bfloat16, + np_dtype_bfloat16, + np_dtype_bfloat16, + (-1, -1), + (-1, -1), + (-1, -1), + 0, + ) if FLAGS.ensemble: # Create utility models used in ensemble diff --git a/qa/common/test_util.py b/qa/common/test_util.py index 326c2b55d3..46a42668bf 100755 --- a/qa/common/test_util.py +++ b/qa/common/test_util.py @@ -30,13 +30,14 @@ import os import unittest +import ml_dtypes import numpy as np _last_request_id = 0 # Numpy does not support the BF16 datatype natively. -# We use this dummy dtype as a representative for BF16. -np_dtype_bfloat16 = np.dtype([("bf16", object)]) +# We use ml_dtypes.bfloat16 for BF16. +np_dtype_bfloat16 = ml_dtypes.bfloat16 MIB = 2**20 # 1 MIB = 1,048,576 bytes GIB = 2**30 # 1 GIB = 1,073,741,824 bytes From a706aed49d3ee00d8f4a2f5ffd1360961fd35ce5 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Mon, 18 May 2026 17:13:10 -0700 Subject: [PATCH 215/230] test: Add C++ gRPC cancellation tests to L0_request_cancellation (#8775) --- docs/user_guide/request_cancellation.md | 19 +++--- qa/L0_request_cancellation/test.sh | 82 ++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 13 deletions(-) diff --git a/docs/user_guide/request_cancellation.md b/docs/user_guide/request_cancellation.md index 753d03968a..10a676ced0 100644 --- a/docs/user_guide/request_cancellation.md +++ b/docs/user_guide/request_cancellation.md @@ -28,14 +28,14 @@ # Request Cancellation -Starting from r23.10, Triton supports handling request cancellation received -from the gRPC client or a C API user. Long running inference requests such -as for auto generative large language models may run for an indeterminate -amount of time or indeterminate number of steps. Additionally clients may -enqueue a large number of requests as part of a sequence or request stream -and later determine the results are no longer needed. Continuing to process -requests whose results are no longer required can significantly impact server -resources. +Triton supports handling request cancellation received from the gRPC Python +client or a C API user (since r23.10), and C++ client (since r26.05). +Long running inference requests such as for auto generative large language +models may run for an indeterminate amount of time or indeterminate number of +steps. Additionally clients may enqueue a large number of requests as part of +a sequence or request stream and later determine the results are no longer +needed. Continuing to process requests whose results are no longer required can +significantly impact server resources. ## Issuing Request Cancellation @@ -51,8 +51,7 @@ about the APIs in [tritonserver.h](https://github.com/triton-inference-server/co In addition, [gRPC endpoint](../customization_guide/inference_protocols.md#httprest-and-grpc-protocols) can now detect cancellation from the client and attempt to terminate request. -At present, only gRPC python client supports issuing request cancellation -to the server endpoint. See [request-cancellation](https://github.com/triton-inference-server/client#request-cancellation) +See [request-cancellation](https://github.com/triton-inference-server/client#request-cancellation) for more details on how to issue requests from the client-side. See gRPC guide on RPC [cancellation](https://grpc.io/docs/guides/cancellation/) for finer details. diff --git a/qa/L0_request_cancellation/test.sh b/qa/L0_request_cancellation/test.sh index d8cffb91e6..2b92c12027 100755 --- a/qa/L0_request_cancellation/test.sh +++ b/qa/L0_request_cancellation/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -42,8 +42,10 @@ export CUDA_VISIBLE_DEVICES=0 SERVER=/opt/tritonserver/bin/tritonserver source ../common/util.sh +CANCEL_LOG_LINE="Cancellation notification received for " RET=0 +rm -f *.log # # Unit tests @@ -66,7 +68,7 @@ if [ $? -ne 0 ]; then fi # -# gRPC cancellation tests +# Python gRPC cancellation tests # rm -rf models && mkdir models mkdir -p models/custom_identity_int32/1 && (cd models/custom_identity_int32 && \ @@ -121,7 +123,7 @@ for TEST_CASE in "test_grpc_async_infer" \ RET=1 fi - count=$(grep -o "Cancellation notification received for" $SERVER_LOG | wc -l) + count=$(grep -o "$CANCEL_LOG_LINE" $SERVER_LOG | wc -l) if [ $count == 0 ]; then echo -e "\n***\n*** Cancellation not received by server on $TEST_CASE\n***" cat $SERVER_LOG @@ -170,6 +172,80 @@ for TEST_CASE in "test_grpc_async_infer" \ fi done +# +# C++ gRPC cancellation tests +# +# allow_timeout_override disables queue prefetching, keeping requests queued +# long enough for the "Queued" cancellation tests to cancel them before +# forwarding to the rate limiter. This saves overall test time. +cat >> models/custom_identity_int32/config.pbtxt <<'EOF' +dynamic_batching { + default_queue_policy { + allow_timeout_override: true + } +} +EOF + +GRPC_CANCELLATION_TEST_CPP=../clients/grpc_cancellation_test + +for ENTRY in "TestGrpcAsyncInferCancelExecutingRequest 1" \ + "TestGrpcAsyncInferCancelQueuedRequest 2" \ + "TestGrpcAsyncInferCancelAfterCompletionIsNoOp 0" \ + "TestGrpcAsyncInferWithoutContextStillCompletes 0" \ + "TestGrpcAsyncInferMultiCancelExecutingRequests 2" \ + "TestGrpcAsyncInferMultiCancelQueuedRequest 2" \ + "TestGrpcStreamInferCancelExecutingRequest 1" \ + "TestGrpcStreamInferCancelQueuedRequest 1" \ + "TestGrpcStreamCancelWithoutInfer 1" \ + "TestGrpcStreamCancelThenRestart 1"; do + read -r TEST_CASE EXPECTED_CANCEL_COUNT <<< "$ENTRY" + + TEST_LOG="./grpc_cancellation_test_cpp.$TEST_CASE.log" + SERVER_LOG="./grpc_cancellation_test_cpp.$TEST_CASE.server.log" + + # AsyncInferMulti fans out N concurrent requests; bump to 3 CPU instances + # so each can execute in parallel. Every other test uses the default + # single-instance config. + if [ "$TEST_CASE" == "TestGrpcAsyncInferMultiCancelExecutingRequests" ]; then + sed -i 's|instance_group .*|instance_group [{ count: 3, kind: KIND_CPU }]|' \ + models/custom_identity_int32/config.pbtxt + fi + + SERVER_ARGS="--model-repository=`pwd`/models --log-verbose=2" + run_server + if [ "$SERVER_PID" == "0" ]; then + echo -e "\n***\n*** Failed to start $SERVER\n***" + cat $SERVER_LOG + exit 1 + fi + + set +e + LD_LIBRARY_PATH=/opt/tritonserver/lib:$LD_LIBRARY_PATH \ + $GRPC_CANCELLATION_TEST_CPP \ + --gtest_filter="GrpcCancellationTest.$TEST_CASE" > $TEST_LOG 2>&1 + if [ $? -ne 0 ]; then + echo -e "\n***\n*** C++ gRPC Cancellation Tests Failed on $TEST_CASE\n***" + cat $TEST_LOG + RET=1 + fi + + cancel_count=$(grep -c "$CANCEL_LOG_LINE" $SERVER_LOG || true) + if [ $cancel_count -ne $EXPECTED_CANCEL_COUNT ]; then + echo -e "\n***\n*** Unexpected cancellation count on $TEST_CASE. Expected $EXPECTED_CANCEL_COUNT but received $cancel_count.\n***" + cat $SERVER_LOG + RET=1 + fi + set -e + + kill $SERVER_PID + wait $SERVER_PID + + if [ "$TEST_CASE" == "TestGrpcAsyncInferMultiCancelExecutingRequests" ]; then + sed -i 's|instance_group .*|instance_group [{ kind: KIND_CPU }]|' \ + models/custom_identity_int32/config.pbtxt + fi +done + # # End-to-end scheduler tests # From c985be3568889c891c14eaca540edb6229de2f0c Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Tue, 19 May 2026 16:26:06 -0700 Subject: [PATCH 216/230] fix: Verify RE2::FullMatch return value in Sagemaker server (#8791) --- src/sagemaker_server.cc | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/sagemaker_server.cc b/src/sagemaker_server.cc index 70b0d329d7..bc3745b78c 100644 --- a/src/sagemaker_server.cc +++ b/src/sagemaker_server.cc @@ -1027,9 +1027,18 @@ SagemakerAPIServer::SageMakerMMELoadModel( */ std::string repo_parent_path, subdir, customer_subdir; - RE2::FullMatch( + bool matched = RE2::FullMatch( url_abspath, model_path_regex_, &repo_parent_path, &subdir, &customer_subdir); + if (!matched) { + HTTP_RESPOND_IF_ERR( + req, TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, + ("Invalid \"url\" '" + url_string + + "': expected a path of the form " + "/opt/ml/models//model[/]") + .c_str())); + } std::string config_path = url_abspath + "/config.pbtxt"; struct stat buffer; From c8e2f021dc5122c8c66536745a0fd1fb0fe4e6da Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Wed, 20 May 2026 12:24:14 -0700 Subject: [PATCH 217/230] docs: Officially drop Windows-related documentation (#8792) --- README.md | 1 - docs/client_guide/in_process.rst | 11 +-- docs/customization_guide/build.md | 113 +++-------------------- docs/customization_guide/deploy.md | 12 --- docs/protocol/extension_shared_memory.md | 4 +- docs/user_guide/trace.md | 2 - 6 files changed, 18 insertions(+), 125 deletions(-) diff --git a/README.md b/README.md index b2f3d818b0..4db3a159c7 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,6 @@ images. - [Install Triton Inference Server without Docker containers](docs/customization_guide/build.md#building-without-docker) - [Build a custom Triton Inference Server Docker container](docs/customization_guide/compose.md) - [Build Triton Inference Server from source](docs/customization_guide/build.md#building-on-unsupported-platforms) -- [Build Triton Inference Server for Windows 10](docs/customization_guide/build.md#building-for-windows-10) - Examples for deploying Triton Inference Server with Kubernetes and Helm on [GCP](deploy/gcp/README.md), [AWS](deploy/aws/README.md), and [NVIDIA FleetCommand](deploy/fleetcommand/README.md) - [Secure Deployment Considerations](docs/customization_guide/deploy.md) diff --git a/docs/client_guide/in_process.rst b/docs/client_guide/in_process.rst index 56ab778440..13d32af5e8 100644 --- a/docs/client_guide/in_process.rst +++ b/docs/client_guide/in_process.rst @@ -1,5 +1,5 @@ .. -.. Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. .. Redistribution and use in source and binary forms, with or without .. modification, are permitted provided that the following conditions @@ -35,11 +35,10 @@ allows Triton to be linked directly into a C/C++/java/python application. This A is called the "Triton Server API" or just "Server API" for short. The API is implemented in the Triton shared library which is built from source contained in the `core -repository `__. On Linux -this library is libtritonserver.so and on Windows it is -tritonserver.dll. In the Triton Docker image the shared library is -found in /opt/tritonserver/lib. The header file that defines and -documents the Server API is +repository `__. On Linux this +library is libtritonserver.so. In the Triton Docker image the shared library is +found in /opt/tritonserver/lib. The header file that defines and documents the +Server API is `tritonserver.h `__. `Java bindings for In-Process Triton Server API <../customization_guide/inprocess_java_api.html#java-bindings-for-in-process-triton-server-api>`__ are built on top of `tritonserver.h` and can be used for Java applications that diff --git a/docs/customization_guide/build.md b/docs/customization_guide/build.md index 948fbafb06..48495c7211 100644 --- a/docs/customization_guide/build.md +++ b/docs/customization_guide/build.md @@ -59,8 +59,6 @@ to build Triton on a platform that is not listed here. * [Ubuntu 22.04, x86-64](#building-for-ubuntu-2204) -* [Windows 10, x86-64](#building-for-windows-10) - If you are developing or debugging Triton, see [Development and Incremental Builds](#development-and-incremental-builds) for information on how to perform incremental build. @@ -261,90 +259,6 @@ For a given version of Triton you can attempt to build with non-supported versions of TensorRT but you may have build or execution issues since non-supported versions are not tested. -## Building for Windows 10 - -For Windows 10, build.py supports both a Docker build and a non-Docker -build in a similar way as described for [Ubuntu](#building-for-ubuntu-2204). The primary -difference is that the minimal/base image used as the base of -Dockerfile.buildbase image can be built from the provided -[Dockerfile.win10.min](https://github.com/triton-inference-server/server/blob/main/Dockerfile.win10.min) -file as described in [Windows 10 "Min" Image](#windows-10-min-image). When running build.py -use the --image flag to specify the tag that you assigned to this -image. For example, --image=base,win10-py3-min. - -### Windows and Docker - -Depending on your version of Windows 10 and your version of Docker you -may need to perform these additional steps before any of the following -step. - -* Set your Docker to work with "Windows containers". Right click on - the whale icon in the lower-right status area and select "Switch to - Windows containers". - -### Windows 10 "Min" Image - -The "min" container describes the base dependencies needed to perform -the Windows build. The Windows min container is -[Dockerfile.win10.min](https://github.com/triton-inference-server/server/blob/main/Dockerfile.win10.min). - -Before building the min container you must download the appropriate -cuDNN and TensorRT versions and place them in the same directory as -Dockerfile.win10.min. - -* For cuDNN the CUDNN_VERSION and CUDNN_ZIP arguments defined in - Dockerfile.win10.min indicate the version of cuDNN that your should - download from https://developer.nvidia.com/rdp/cudnn-download. - -* For TensorRT the TENSORRT_VERSION and TENSORRT_ZIP arguments defined - in Dockerfile.win10.min indicate the version of TensorRT that your - should download from - https://developer.nvidia.com/nvidia-tensorrt-download. - -After downloading the zip files for cuDNN and TensorRT, you build the -min container using the following command. - -```bash -$ docker build -t win10-py3-min -f Dockerfile.win10.min . -``` - -### Build Triton Server - -Triton is built using the build.py script. The build system must have -Docker, Python3 (plus pip installed *docker* module) and git installed -so that it can execute build.py and perform a docker build. By -default, build.py does not enable any of Triton's optional features -and so you must enable them explicitly. The following build.py -invocation builds all features and backends available on windows. - -```bash -python build.py --cmake-dir=/build --build-dir=/tmp/citritonbuild --no-container-pull --image=base,win10-py3-min --enable-logging --enable-stats --enable-tracing --enable-gpu --endpoint=grpc --endpoint=http --repo-tag=common: --repo-tag=core: --repo-tag=backend: --repo-tag=thirdparty: --backend=ensemble --backend=tensorrt: --backend=onnxruntime: --backend=openvino: --backend=python: -``` - -If you are building on *main* branch then `` will -default to "main". If you are building on a release branch then -`` will default to the branch name. For example, if you -are building on the r24.12 branch, `` will default to -r24.12. Therefore, you typically do not need to provide `` at all (nor the preceding colon). You can use a different -`` for a component to instead use the corresponding -branch/tag in the build. For example, if you have a branch called -"mybranch" in the -[onnxruntime_backend](https://github.com/triton-inference-server/onnxruntime_backend) -repo that you want to use in the build, you would specify ---backend=onnxruntime:mybranch. - -### Extract Build Artifacts - -When build.py completes, a Docker image called *tritonserver* will -contain the built Triton Server executable, libraries and other -artifacts. Windows containers do not support GPU access so you likely -want to extract the necessary files from the tritonserver image and -run them directly on your host system. All the Triton artifacts can be -found in /opt/tritonserver directory of the tritonserver image. Your -host system will need to install the CUDA, cuDNN, TensorRT and other -dependencies that were used for the build. - ## Building on Unsupported Platforms Building for an unsupported OS and/or hardware platform is @@ -409,8 +323,8 @@ and cmake_build or the equivalent commands to perform a build. If you are [building without Docker](#building-without-docker) use the CMake invocation steps in cmake_build to invoke CMake to set-up a -build environment where you can invoke make/msbuild.exe to incremental -build the Triton core, a backend, or a repository agent. +build environment where you can invoke make to incremental build the Triton +core, a backend, or a repository agent. ### Development Builds With Docker @@ -423,8 +337,7 @@ agents. To perform an incremental build within the *tritonserver_buildbase* container, map your source into the container and then run the -appropriate CMake and `make` (or `msbuild.exe`) steps from cmake_build -within the container. +appropriate CMake and `make` steps from cmake_build within the container. #### Development Build of Triton Core @@ -450,10 +363,9 @@ CMakeLists.txt file and source: $ cmake /server ``` -Then you can change directory into the build directory and run `make` -(or `msbuild.exe`) as shown in cmake_build. As you make changes to the -source on your host system, you can perform incremental builds by -re-running `make` (or `msbuild.exe`). +Then you can change directory into the build directory and run `make` as shown +in cmake_build. As you make changes to the source on your host system, you can +perform incremental builds by re-running `make`. #### Development Build of Backend or Repository Agent @@ -467,10 +379,8 @@ incremental builds to test those changes. Your source code is in /home/me/tritonserver_backend. Run the *tritonserver_buildbase* container and map your TensorRT backend source directory into the container at /tensorrt_backend. Note that some backends will use -Docker as part of their build, and so the host's Docker registry must -be made available within the *tritonserver_buildbase* by mounting -docker.sock (on Windows use --v\\.\pipe\docker_engine:\\.\pipe\docker_engine). +Docker as part of their build, and so the host's Docker registry must be made +available within the *tritonserver_buildbase* by mounting docker.sock. ``` $ docker run -it --rm -v/var/run/docker.sock:/var/run/docker.sock -v/home/me/tensorrt_backend:/tensorrt_backend tritonserver_buildbase bash @@ -487,10 +397,9 @@ CMakeLists.txt file and source: $ cmake /tensorrt_backend ``` -Then you can change directory into the build directory and run `make` -(or `msbuild.exe`) as shown in cmake_build. As you make changes to the -source on your host system, you can perform incremental builds by -re-running `make` (or `msbuild.exe`). +Then you can change directory into the build directory and run `make` as shown +in cmake_build. As you make changes to the source on your host system, you can +perform incremental builds by re-running `make`. ### Building with Debug Symbols diff --git a/docs/customization_guide/deploy.md b/docs/customization_guide/deploy.md index 2a305de5d3..2a9553bb2f 100644 --- a/docs/customization_guide/deploy.md +++ b/docs/customization_guide/deploy.md @@ -279,18 +279,6 @@ Directory where cache shared libraries are found. > must be access controlled. Adding untrusted files > can lead to arbitrarty code execution. -##### `backend-config=,additional-dependency-dirs=` - -This is an optional Windows feature that enables Triton to search custom -dependency directories when loading a specific backend. The user can input -these directories as a string of semicolon-separated paths (including a -trailing semicolon). These directories are programmatically prepended to -the process's PATH and are removed when the backend is loaded successfully. -Windows will search PATH last in its search sequence, so be cautious that -no untrusted files of same name exist in a location of higher search priority -(e.g., System32). It is still recommended to add backend-specific dependencies -to their corresponding backend folder when possible. - # GRPC server options Triton Inference Server's gRPC inference handlers internally use states to manage inference requests and response queues. Each state consists of one inference request and one response queue. The response queue within a state can hold multiple response objects. These states remain allocated for reuse to optimize performance by minimizing dynamic allocations. diff --git a/docs/protocol/extension_shared_memory.md b/docs/protocol/extension_shared_memory.md index cb5d9ccc7f..d7ae7c045e 100644 --- a/docs/protocol/extension_shared_memory.md +++ b/docs/protocol/extension_shared_memory.md @@ -64,8 +64,8 @@ given Triton will return an error. > [!NOTE] > -> Shared memory is not supported on Windows. On Jetson, only system shared -> memory is supported (CUDA shared memory is not). +> On Jetson, only system shared memory is supported (CUDA shared memory is +> not). ## HTTP/REST diff --git a/docs/user_guide/trace.md b/docs/user_guide/trace.md index 2f3413385d..8db98e7705 100644 --- a/docs/user_guide/trace.md +++ b/docs/user_guide/trace.md @@ -641,8 +641,6 @@ spans are properly ended. ### Limitations -- OpenTelemetry trace mode is not supported on Windows systems. - - Triton supports only [OTLP/HTTP Exporter](https://opentelemetry.io/docs/specs/otlp/#otlphttp) and allows specification of only url for this exporter through From 2d64d2deb0fee30487621657cd35a48aadfc7a64 Mon Sep 17 00:00:00 2001 From: J Wyman Date: Thu, 21 May 2026 10:07:51 -0400 Subject: [PATCH 218/230] fix: Replace std:atoi with std:stoi (#8794) --- src/http_server.cc | 4 ++-- src/sagemaker_server.cc | 4 ++-- src/vertex_ai_server.cc | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/http_server.cc b/src/http_server.cc index d351956449..b8d9ea00da 100644 --- a/src/http_server.cc +++ b/src/http_server.cc @@ -2440,7 +2440,7 @@ HTTPAPIServer::GetContentLength( evhtp_kv_find(req->headers_in, kContentLengthHeader); if (content_length_c_str != nullptr) { try { - lcontent_length = std::atoi(content_length_c_str); + lcontent_length = std::stoi(content_length_c_str); } catch (const std::invalid_argument& ia) { err = TRITONSERVER_ErrorNew( @@ -2484,7 +2484,7 @@ HTTPAPIServer::GetInferenceHeaderLength( if (header_length_c_str != NULL) { int parsed_value; try { - parsed_value = std::atoi(header_length_c_str); + parsed_value = std::stoi(header_length_c_str); } catch (const std::invalid_argument& ia) { return TRITONSERVER_ErrorNew( diff --git a/src/sagemaker_server.cc b/src/sagemaker_server.cc index bc3745b78c..6037a195ef 100644 --- a/src/sagemaker_server.cc +++ b/src/sagemaker_server.cc @@ -98,7 +98,7 @@ SagemakerAPIServer::GetInferenceHeaderLength( int32_t parsed_value; try { parsed_value = - std::atoi(content_type_c_str + binary_mime_type_.length()); + std::stoi(content_type_c_str + binary_mime_type_.length()); } catch (const std::invalid_argument& ia) { return TRITONSERVER_ErrorNew( @@ -508,7 +508,7 @@ SagemakerAPIServer::SageMakerMMEHandleInfer( evhtp_kv_find(req->headers_in, kContentLengthHeader); if (content_length_c_str != nullptr) { try { - content_length = std::atoi(content_length_c_str); + content_length = std::stoi(content_length_c_str); } catch (const std::invalid_argument& ia) { err = TRITONSERVER_ErrorNew( diff --git a/src/vertex_ai_server.cc b/src/vertex_ai_server.cc index ec4fd6802f..2e59a84e45 100644 --- a/src/vertex_ai_server.cc +++ b/src/vertex_ai_server.cc @@ -79,7 +79,7 @@ VertexAiAPIServer::GetInferenceHeaderLength( int32_t parsed_value; try { parsed_value = - std::atoi(content_type_c_str + binary_mime_type_.length()); + std::stoi(content_type_c_str + binary_mime_type_.length()); } catch (const std::invalid_argument& ia) { return TRITONSERVER_ErrorNew( From e24fd86daf77f7efe9bceaf0d93384c66cdbc721 Mon Sep 17 00:00:00 2001 From: Tanay <94170547+Its-Tanay@users.noreply.github.com> Date: Thu, 28 May 2026 02:16:22 +0530 Subject: [PATCH 219/230] fix: ignore SIGPIPE to prevent server crash on S3 idle connection timeout (#8768) --- src/triton_signal.cc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/triton_signal.cc b/src/triton_signal.cc index 1a02144d2a..05fcb97444 100644 --- a/src/triton_signal.cc +++ b/src/triton_signal.cc @@ -1,4 +1,4 @@ -// Copyright 2020-2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2020-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -137,6 +137,9 @@ RegisterSignalHandler() signal(SIGSEGV, ErrorSignalHandler); signal(SIGABRT, ErrorSignalHandler); + // Ignore SIGPIPE to prevent crash on AWS SDK writing to closed idle sockets + signal(SIGPIPE, SIG_IGN); + return nullptr; // success } From 9082a24b295955b43ab29140f8225ec8685e2817 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Wed, 27 May 2026 14:23:48 -0700 Subject: [PATCH 220/230] test: Special handling for ONNX bf16 models in tests (#8800) --- qa/L0_http_fuzz/test.sh | 4 ++-- qa/L0_infer/test.sh | 39 +++++++++++++++++++++++------------- qa/L0_infer_variable/test.sh | 7 ++++++- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/qa/L0_http_fuzz/test.sh b/qa/L0_http_fuzz/test.sh index 1abe08b487..96433aaed9 100755 --- a/qa/L0_http_fuzz/test.sh +++ b/qa/L0_http_fuzz/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2020-2025, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2020-2026, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -65,7 +65,7 @@ function_install_python38() { # Install test script dependencies pip3 install --upgrade wheel setuptools boofuzz==0.3.0 "numpy<2" pillow attrdict future grpcio requests gsutil \ - awscli six grpcio-channelz prettytable virtualenv + awscli six grpcio-channelz prettytable virtualenv ml_dtypes } function_install_python38 diff --git a/qa/L0_infer/test.sh b/qa/L0_infer/test.sh index 96c5e97efd..691eca4b72 100755 --- a/qa/L0_infer/test.sh +++ b/qa/L0_infer/test.sh @@ -163,6 +163,9 @@ function generate_model_repository() { # Types that need to use SubAdd instead of AddSub swap_types="float32 int32 int16 int8" for onnx_model in $onnx_models; do + # Skip BF16 models: Not yet supported by Python backend + [[ "$onnx_model" == *bf16* ]] && continue + if [ "$BACKEND" == "python_dlpack" ]; then python_model=`echo $onnx_model | sed 's/onnx/python_dlpack/g' | sed 's,'"$DATADIR/qa_model_repository/"',,g'` else @@ -215,6 +218,10 @@ function generate_model_repository() { else cp -r ${DATADIR}/qa_model_repository/${BACKEND}* \ models/. + # Remove ONNX BF16 models from CPU models + if [ "$BACKEND" == "onnx" ] && [ "$TARGET" == "cpu" ]; then + rm -rf models/onnx_*bf16* + fi fi done @@ -253,20 +260,24 @@ function generate_model_repository() { KIND="KIND_GPU" && [[ "$TARGET" == "cpu" ]] && KIND="KIND_CPU" for FW in $BACKENDS; do - if [ "$FW" == "onnx" ] && [ "$TEST_VALGRIND" -eq 1 ]; then - # Reduce the instance count to make loading onnx models faster - for MC in `ls models/${FW}*/config.pbtxt`; do - echo "instance_group [ { kind: ${KIND} count: 1 }]" >> $MC - done - elif [ "$FW" != "plan" ] && [ "$FW" != "python" ] && [ "$FW" != "python_dlpack" ] && [ "$FW" != "openvino" ];then - for MC in `ls models/${FW}*/config.pbtxt`; do - echo "instance_group [ { kind: ${KIND} }]" >> $MC - done - elif [ "$FW" == "python" ] || [ "$FW" == "python_dlpack" ] || [ "$FW" == "openvino" ]; then - for MC in `ls models/${FW}*/config.pbtxt`; do - echo "instance_group [ { kind: KIND_CPU }]" >> $MC - done - fi + [ "$FW" == "plan" ] && continue + for MC in `ls models/${FW}*/config.pbtxt`; do + # BF16 models: ORT CPU has no BF16 kernels, force GPU + if [ "$FW" == "onnx" ] && [ "$MC" == *bf16* ]; then + MC_KIND="KIND_GPU" + else + MC_KIND=${KIND} + fi + + if [ "$FW" == "onnx" ] && [ "$TEST_VALGRIND" -eq 1 ]; then + # Reduce the instance count to make loading onnx models faster + echo "instance_group [ { kind: ${MC_KIND} count: 1 }]" >> $MC + elif [ "$FW" == "python" ] || [ "$FW" == "python_dlpack" ] || [ "$FW" == "openvino" ]; then + echo "instance_group [ { kind: KIND_CPU }]" >> $MC + else + echo "instance_group [ { kind: ${KIND} }]" >> $MC + fi + done done # Modify custom_zero_1_float32 and custom_nobatch_zero_1_float32 for relevant ensembles diff --git a/qa/L0_infer_variable/test.sh b/qa/L0_infer_variable/test.sh index 36d86716ad..4498f1a57f 100755 --- a/qa/L0_infer_variable/test.sh +++ b/qa/L0_infer_variable/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2026, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -64,6 +64,11 @@ for TARGET in cpu gpu; do cp -r /data/inferenceserver/${REPO_VERSION}/qa_variable_model_repository models && \ cp -r /data/inferenceserver/${REPO_VERSION}/qa_ensemble_model_repository/qa_variable_model_repository/* models/. + # Remove ONNX BF16 models from CPU models + if [ "$TARGET" == "cpu" ]; then + rm -rf models/onnx_*bf16* + fi + create_nop_version_dir `pwd`/models KIND="KIND_GPU" && [[ "$TARGET" == "cpu" ]] && KIND="KIND_CPU" From 8cb2b77c5f35dbf034c943f69ce51fb7990c9261 Mon Sep 17 00:00:00 2001 From: Sai Kiran Polisetty Date: Fri, 29 May 2026 16:03:56 +0530 Subject: [PATCH 221/230] feat: Add HTTP request body size limit to OpenAI frontend (#8787) --- python/openai/README.md | 25 +++ .../fastapi/middleware/request_size.py | 163 +++++++++++++++++ .../frontend/fastapi_frontend.py | 12 ++ python/openai/openai_frontend/main.py | 10 ++ python/openai/openai_frontend/utils/utils.py | 17 +- python/openai/tests/test_request_size.py | 165 ++++++++++++++++++ qa/L0_openai/test.sh | 15 +- 7 files changed, 399 insertions(+), 8 deletions(-) create mode 100644 python/openai/openai_frontend/frontend/fastapi/middleware/request_size.py create mode 100644 python/openai/tests/test_request_size.py diff --git a/python/openai/README.md b/python/openai/README.md index 4598a5a43f..d32ce97356 100644 --- a/python/openai/README.md +++ b/python/openai/README.md @@ -949,3 +949,28 @@ curl -H "api-key: my-secret-key" \ # Multiple APIs in single argument with shared authentication --openai-restricted-api "inference,model-repository shared-key shared-secret" ``` + +## HTTP Request Body Size Limit + +The frontend enforces a maximum request body size prior to JSON parsing. Requests that exceed this limit are rejected with an error response. + +Use `--http-max-input-size` to configure the limit (default: `67108864` bytes / 64 MiB): + +```bash +python3 openai_frontend/main.py \ + --model-repository /path/to/models \ + --tokenizer meta-llama/Meta-Llama-3.1-8B-Instruct \ + --http-max-input-size 67108864 +``` + +The limit applies to all endpoints. Example error response: + +```json +{ + "error": { + "message": "Request content size exceeds the maximum allowed input size of 67108864 bytes. Use --http-max-input-size to increase the limit.", + "type": "invalid_request_error", + "code": "content_too_large" + } +} +``` diff --git a/python/openai/openai_frontend/frontend/fastapi/middleware/request_size.py b/python/openai/openai_frontend/frontend/fastapi/middleware/request_size.py new file mode 100644 index 0000000000..333a7968f4 --- /dev/null +++ b/python/openai/openai_frontend/frontend/fastapi/middleware/request_size.py @@ -0,0 +1,163 @@ +# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from fastapi.responses import JSONResponse +from starlette.types import ASGIApp, Message, Receive, Scope, Send +from utils.utils import StatusCode, validate_positive_int + + +async def _disconnect_receive() -> Message: + return {"type": "http.disconnect"} + + +class RequestSizeLimitMiddleware: + """ + Reject HTTP requests whose body exceeds ``http_max_input_size`` bytes. + First validation rejects on the Content-Length header before any body bytes are + read. Second validation streams the body chunks, counting bytes as they arrive, + and rejects as soon as the running total crosses the limit. Driving + receive() from the middleware protects every endpoint, including + handlers that never read the body. The buffered body is released the + moment the application consumes it, so the middleware contributes no + sustained memory overhead. + """ + + def __init__(self, app: ASGIApp, http_max_input_size: int) -> None: + self.app = app + self.http_max_input_size = validate_positive_int(http_max_input_size) + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] != "http": + await self.app(scope, receive, send) + return + + # Stage 1: reject on Content-Length before reading any body bytes. + for name, value in scope["headers"]: + if name != b"content-length": + continue + try: + content_length = int(value) + except ValueError: + await self._send_error( + scope, + send, + StatusCode.CLIENT_ERROR, + "invalid_content_length", + "Invalid Content-Length header: not an integer.", + ) + return + if content_length < 0: + await self._send_error( + scope, + send, + StatusCode.CLIENT_ERROR, + "invalid_content_length", + "Invalid Content-Length header: must be non-negative.", + ) + return + if content_length > self.http_max_input_size: + await self._send_error( + scope, + send, + StatusCode.CONTENT_TOO_LARGE, + "content_too_large", + self._oversized_request_message( + content_length, self.http_max_input_size + ), + ) + return + break + + # Stage 2: count chunks as they arrive, reject if total exceeds limit. + body_chunks: list[bytes] = [] + total = 0 + while True: + message = await receive() + if message["type"] != "http.request": + return + chunk = message.get("body", b"") + total += len(chunk) + if total > self.http_max_input_size: + await self._send_error( + scope, + send, + StatusCode.CONTENT_TOO_LARGE, + "content_too_large", + self._oversized_request_message(total, self.http_max_input_size), + ) + return + body_chunks.append(chunk) + if not message.get("more_body", False): + break + + # Assemble the buffered body and replay it to the app. + body_message: Message = { + "type": "http.request", + "body": b"".join(body_chunks), + "more_body": False, + } + del body_chunks + + async def replay_receive() -> Message: + nonlocal body_message + if body_message is not None: + # Drop the reference on hand-off so the body is freed while + # the app processes it, instead of being held by this closure. + message, body_message = body_message, None + return message + # Body already delivered — delegate to the original receive() so + # streaming responses can wait for the real client disconnect. + return await receive() + + await self.app(scope, replay_receive, send) + + @staticmethod + def _oversized_request_message(actual_bytes: int, max_bytes: int) -> str: + return ( + f"Request size of {actual_bytes} bytes exceeds the maximum allowed " + f"input size of {max_bytes} bytes. " + f"Use --http-max-input-size to increase the limit." + ) + + async def _send_error( + self, + scope: Scope, + send: Send, + status_code: int, + code: str, + message: str, + ) -> None: + response = JSONResponse( + status_code=status_code, + content={ + "error": { + "message": message, + "type": "invalid_request_error", + "code": code, + } + }, + ) + await response(scope, _disconnect_receive, send) diff --git a/python/openai/openai_frontend/frontend/fastapi_frontend.py b/python/openai/openai_frontend/frontend/fastapi_frontend.py index f7fa8aab3c..752befd8bc 100644 --- a/python/openai/openai_frontend/frontend/fastapi_frontend.py +++ b/python/openai/openai_frontend/frontend/fastapi_frontend.py @@ -34,6 +34,7 @@ APIRestrictionMiddleware, RestrictedFeatures, ) +from frontend.fastapi.middleware.request_size import RequestSizeLimitMiddleware from frontend.fastapi.routers import ( chat, completions, @@ -43,6 +44,7 @@ observability, ) from frontend.frontend import OpenAIFrontend +from utils.utils import HTTP_DEFAULT_MAX_INPUT_SIZE class FastApiFrontend(OpenAIFrontend): @@ -53,10 +55,12 @@ def __init__( port: int = 8000, log_level: str = "info", restricted_apis: list = None, + http_max_input_size: int = HTTP_DEFAULT_MAX_INPUT_SIZE, ): self.host: str = host self.port: int = port self.log_level: str = log_level + self.http_max_input_size: int = http_max_input_size if restricted_apis: self.restricted_apis: RestrictedFeatures = RestrictedFeatures( restricted_apis @@ -111,6 +115,7 @@ def _create_app(self): self._add_cors_middleware(app) if self.restricted_apis != None: self._add_api_restriction_middleware(app) + self._add_request_size_limit_middleware(app) return app @@ -137,3 +142,10 @@ def _add_api_restriction_middleware(self, app: FastAPI): print( f"[INFO] API restrictions enabled. Restricted API endpoints: {self.restricted_apis.RestrictionDict()}" ) + + def _add_request_size_limit_middleware(self, app: FastAPI): + app.add_middleware( + RequestSizeLimitMiddleware, + http_max_input_size=self.http_max_input_size, + ) + print(f"[INFO] HTTP request size limit set to {self.http_max_input_size} bytes") diff --git a/python/openai/openai_frontend/main.py b/python/openai/openai_frontend/main.py index ac310e662c..cf9228b9fc 100755 --- a/python/openai/openai_frontend/main.py +++ b/python/openai/openai_frontend/main.py @@ -34,6 +34,7 @@ import tritonserver from engine.triton_engine import TritonLLMEngine from frontend.fastapi_frontend import FastApiFrontend +from utils.utils import HTTP_DEFAULT_MAX_INPUT_SIZE, validate_positive_int def signal_handler( @@ -197,6 +198,14 @@ def parse_args(): action="append", help="Restrict access to specific OpenAI API endpoints. Format: ',,... ' (e.g., 'inference,model-repository admin-key admin-value'). If not specified, all endpoints are allowed.", ) + openai_group.add_argument( + "--http-max-input-size", + type=validate_positive_int, + default=HTTP_DEFAULT_MAX_INPUT_SIZE, + help=f"Maximum allowed HTTP request input size in bytes for the OpenAI " + f"frontend (default: {HTTP_DEFAULT_MAX_INPUT_SIZE}, i.e. 64 MiB). " + "Requests exceeding this limit will be rejected.", + ) # KServe Predict v2 Frontend kserve_group = parser.add_argument_group("Triton KServe Frontend") @@ -269,6 +278,7 @@ def main(): port=args.openai_port, log_level=args.uvicorn_log_level, restricted_apis=args.openai_restricted_api, + http_max_input_size=args.http_max_input_size, ) except ValueError as e: print( diff --git a/python/openai/openai_frontend/utils/utils.py b/python/openai/openai_frontend/utils/utils.py index c8fd3609f6..86b4d921e4 100644 --- a/python/openai/openai_frontend/utils/utils.py +++ b/python/openai/openai_frontend/utils/utils.py @@ -1,4 +1,4 @@ -# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -26,6 +26,10 @@ from enum import IntEnum +# Default value for the --http-max-input-size CLI flag (64 MiB). +# Same as HTTP_DEFAULT_MAX_INPUT_SIZE in src/common.h. +HTTP_DEFAULT_MAX_INPUT_SIZE: int = 1 << 26 + class ServerError(Exception): """Exception raised for server errors.""" @@ -44,4 +48,15 @@ class StatusCode(IntEnum): CLIENT_ERROR = 400 AUTHORIZATION_ERROR = 401 NOT_FOUND = 404 + CONTENT_TOO_LARGE = 413 SERVER_ERROR = 500 + + +def validate_positive_int(value: object) -> int: + try: + ivalue = int(value) + except (TypeError, ValueError): + raise ValueError(f"value is not an integer, got {value!r}") + if ivalue <= 0: + raise ValueError(f"value must be greater than 0, got {value!r}") + return ivalue diff --git a/python/openai/tests/test_request_size.py b/python/openai/tests/test_request_size.py new file mode 100644 index 0000000000..5670d00a7a --- /dev/null +++ b/python/openai/tests/test_request_size.py @@ -0,0 +1,165 @@ +# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +import asyncio +import json +import os +import sys +from pathlib import Path + +import pytest +import tritonserver +from fastapi.testclient import TestClient + +sys.path.append( + os.path.join(str(Path(__file__).resolve().parent.parent), "openai_frontend") +) + +from frontend.fastapi.middleware.request_size import RequestSizeLimitMiddleware +from tests.utils import setup_fastapi_app, setup_server +from utils.utils import HTTP_DEFAULT_MAX_INPUT_SIZE + +_MODEL = "mock_llm" + +# All POST endpoints. +_ENDPOINTS = ( + "/v1/chat/completions", + "/v1/completions", + "/v1/embeddings", + f"/v1/models/{_MODEL}/load", + f"/v1/models/{_MODEL}/unload", +) + + +@pytest.fixture(scope="module") +def client(): + """FastApiFrontend backed by a real Triton server with mock_llm loaded.""" + model_repository = str(Path(__file__).parent / "test_models") + server = setup_server( + model_repository, + model_control_mode=tritonserver.ModelControlMode.EXPLICIT, + load_models=[_MODEL], + ) + try: + app = setup_fastapi_app(tokenizer="", server=server, backend=None) + with TestClient(app) as test_client: + yield test_client + finally: + server.stop() + + +def _assert_content_too_large(response, actual_bytes: int) -> None: + assert response.status_code == 413 + body = response.json() + assert set(body) == {"error"} + error = body["error"] + assert error["type"] == "invalid_request_error" + assert error["code"] == "content_too_large" + assert error["message"] == RequestSizeLimitMiddleware._oversized_request_message( + actual_bytes, HTTP_DEFAULT_MAX_INPUT_SIZE + ) + + +class TestRequestSizeLimitMiddleware: + @pytest.mark.parametrize("endpoint", _ENDPOINTS) + def test_body_at_limit_is_not_rejected(self, client, endpoint): + response = client.post(endpoint, content=b"x" * HTTP_DEFAULT_MAX_INPUT_SIZE) + assert response.status_code != 413 + + @pytest.mark.parametrize("endpoint", _ENDPOINTS) + def test_body_over_limit_is_rejected(self, client, endpoint): + over = HTTP_DEFAULT_MAX_INPUT_SIZE + 1 + response = client.post(endpoint, content=b"x" * over) + _assert_content_too_large(response, over) + + @pytest.mark.parametrize("endpoint", _ENDPOINTS) + def test_chunked_body_over_limit_is_rejected(self, client, endpoint): + over = HTTP_DEFAULT_MAX_INPUT_SIZE + 1 + + # httpx switches to chunked transfer when content is an Iterable[bytes]. + def chunks(): + yield b"x" * HTTP_DEFAULT_MAX_INPUT_SIZE + yield b"x" + + response = client.post(endpoint, content=chunks()) + _assert_content_too_large(response, over) + + def test_get_without_body_is_unaffected(self, client): + response = client.get(_ENDPOINTS[0]) + assert response.status_code == 405 + + +class TestContentLengthValidation: + """Stage 1 rejects malformed Content-Length with 400.""" + + def _run_with_content_length(self, raw_value: bytes) -> tuple[int, dict]: + captured: dict = {"status": None, "body": b""} + + async def app(scope, receive, send): + raise AssertionError("app must not be reached for invalid Content-Length") + + async def receive(): + raise AssertionError("receive() must not be called when Stage 1 rejects") + + async def send(message): + if message["type"] == "http.response.start": + captured["status"] = message["status"] + elif message["type"] == "http.response.body": + captured["body"] += message.get("body", b"") + + middleware = RequestSizeLimitMiddleware( + app=app, http_max_input_size=HTTP_DEFAULT_MAX_INPUT_SIZE + ) + scope = { + "type": "http", + "method": "POST", + "path": "/v1/chat/completions", + "headers": [(b"content-length", raw_value)], + } + asyncio.run(middleware(scope, receive, send)) + + assert captured["status"] is not None, "no response was sent" + return captured["status"], json.loads(captured["body"]) + + def _assert_invalid_content_length(self, status: int, body: dict): + assert status == 400 + assert set(body) == {"error"} + error = body["error"] + assert error["type"] == "invalid_request_error" + assert error["code"] == "invalid_content_length" + + @pytest.mark.parametrize("raw", [b"not-a-number", b""]) + def test_non_integer_content_length_rejected_with_400(self, raw): + status, body = self._run_with_content_length(raw) + self._assert_invalid_content_length(status, body) + assert "not an integer" in body["error"]["message"] + + @pytest.mark.parametrize("raw", [b"-1", b"-1024"]) + def test_negative_content_length_rejected_with_400(self, raw): + status, body = self._run_with_content_length(raw) + self._assert_invalid_content_length(status, body) + assert "non-negative" in body["error"]["message"] diff --git a/qa/L0_openai/test.sh b/qa/L0_openai/test.sh index 1862698fa7..5fd4daf239 100755 --- a/qa/L0_openai/test.sh +++ b/qa/L0_openai/test.sh @@ -123,12 +123,13 @@ function pre_test() { function run_test() { pushd openai/ TEST_LOG="test_openai.log" + TEST_XML="test_openai.xml" + TEST_LOG_MISTRAL="test_openai_mistral.log" + TEST_XML_MISTRAL="test_openai_mistral.xml" - # Capture error code without exiting to allow log collection set +e - pytest -s -v --junitxml=test_openai.xml tests/ 2>&1 > ${TEST_LOG} - if [ $? -ne 0 ]; then - cat ${TEST_LOG} + pytest -s -v --junitxml=${TEST_XML} tests/ 2>&1 | tee ${TEST_LOG} + if [ ${PIPESTATUS[0]} -ne 0 ]; then echo -e "\n***\n*** Test Failed\n***" RET=1 fi @@ -137,9 +138,9 @@ function run_test() { if [ "$RET" == "0" ]; then # rerun the tool calling tests with mistral model to cover the mistral tool call parser set +e - TEST_TOOL_CALL_PARSER="mistral" TEST_TOKENIZER="mistralai/Mistral-Nemo-Instruct-2407" pytest -s -v --junitxml=test_openai.xml tests/test_tool_calling.py 2>&1 > ${TEST_LOG} - if [ $? -ne 0 ]; then - cat ${TEST_LOG} + TEST_TOOL_CALL_PARSER="mistral" TEST_TOKENIZER="mistralai/Mistral-Nemo-Instruct-2407" \ + pytest -s -v --junitxml=${TEST_XML_MISTRAL} tests/test_tool_calling.py 2>&1 | tee ${TEST_LOG_MISTRAL} + if [ ${PIPESTATUS[0]} -ne 0 ]; then echo -e "\n***\n*** Test Failed\n***" RET=1 fi From e520f8c70590671f9d9a1277d52422cbaa5f44ef Mon Sep 17 00:00:00 2001 From: Sai Kiran Polisetty Date: Tue, 2 Jun 2026 11:18:40 +0530 Subject: [PATCH 222/230] test: Verify `EXECUTION_ENV_PATH` archive entries stay within the model directory (#8793) --- qa/L0_backend_python/env/test.sh | 57 +++++++++++++++ qa/L0_backend_python/env/zipslip_test.py | 88 ++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100755 qa/L0_backend_python/env/zipslip_test.py diff --git a/qa/L0_backend_python/env/test.sh b/qa/L0_backend_python/env/test.sh index 99c1118b80..b98ba607d6 100755 --- a/qa/L0_backend_python/env/test.sh +++ b/qa/L0_backend_python/env/test.sh @@ -246,6 +246,63 @@ set -e aws s3 rm "${BUCKET_URL_SLASH}" --recursive --include "*" aws s3 rb "${BUCKET_URL}" +# EXECUTION_ENV_PATH path-traversal regression test. For each +# traversal vector, load a Python-backend model with archive is malicious +# and assert: (a) Triton refuses to start, (b) no file was written outside +# the extraction directory, (c) the server log contains the libarchive +# rejection. See zipslip_test.py for the archive layout. +ZIPSLIP_REPO="`pwd`/zipslip_models" +ZIPSLIP_EXECUTION_ENV_PATH='$$TRITON_MODEL_DIRECTORY/malicious_env.tar.gz' + +ZIPSLIP_CASES=( + "relative:Path contains '..'" + "absolute:Path is absolute" +) + +for case in "${ZIPSLIP_CASES[@]}"; do + mode="${case%%:*}" + expected_msg="${case#*:}" + model="zipslip_${mode}" + marker="/tmp/zipslip_${mode}_marker_$$" + SERVER_LOG="./zipslip_${mode}_server.log" + + rm -rf "${marker}" "${SERVER_LOG}" "${ZIPSLIP_REPO}" + mkdir -p "${ZIPSLIP_REPO}/${model}/1" + + cp ../../python_models/identity_fp32/config.pbtxt "${ZIPSLIP_REPO}/${model}/config.pbtxt" + cp ../../python_models/identity_fp32/model.py "${ZIPSLIP_REPO}/${model}/1/model.py" + (cd "${ZIPSLIP_REPO}/${model}" && \ + sed -i "s/^name:.*/name: \"${model}\"/" config.pbtxt && \ + echo "parameters: {key: \"EXECUTION_ENV_PATH\", value: {string_value: \"$ZIPSLIP_EXECUTION_ENV_PATH\"}}" >> config.pbtxt) + python3 ./zipslip_test.py \ + --mode "${mode}" \ + --output "${ZIPSLIP_REPO}/${model}/malicious_env.tar.gz" \ + --marker "${marker}" + + SERVER_ARGS="--model-repository=${ZIPSLIP_REPO} --log-verbose=1" + run_server + if [ "$SERVER_PID" != "0" ]; then + kill_server + echo -e "\n***\n*** Zip Slip (mode=${mode}): tritonserver started despite malicious EXECUTION_ENV_PATH archive.\n***" + RET=1 + fi + + set +e + if [ -e "${marker}" ]; then + ls -la "${marker}" + echo -e "\n***\n*** Zip Slip (mode=${mode}): marker file written outside extraction directory.\n***" + RET=1 + fi + if ! grep -q "${expected_msg}" "${SERVER_LOG}"; then + cat "${SERVER_LOG}" + echo -e "\n***\n*** Zip Slip (mode=${mode}): expected '${expected_msg}' not found in server log.\n***" + RET=1 + fi + set -e + + rm -f "${marker}" +done + if [ $RET -eq 0 ]; then echo -e "\n***\n*** Env Manager Test PASSED.\n***" else diff --git a/qa/L0_backend_python/env/zipslip_test.py b/qa/L0_backend_python/env/zipslip_test.py new file mode 100755 index 0000000000..83336a245f --- /dev/null +++ b/qa/L0_backend_python/env/zipslip_test.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" +Build a malicious EXECUTION_ENV_PATH tarball for the Python backend path-traversal regression test. + + --mode relative -> entry '../../' (ARCHIVE_EXTRACT_SECURE_NODOTDOT) + --mode absolute -> entry '/tmp/' (ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS) +""" + +import argparse +import io +import os +import sys +import tarfile +import time + + +def build_archive(mode: str, output: str, marker: str) -> None: + if mode == "relative": + entry_name = "../../" + marker[len("/tmp/") :] + else: + entry_name = marker + + def add(tar: tarfile.TarFile, name: str, body: bytes, mode: int = 0o644): + info = tarfile.TarInfo(name=name) + info.size, info.mode, info.mtime = len(body), mode, int(time.time()) + tar.addfile(info, io.BytesIO(body)) + + os.makedirs(os.path.dirname(output) or ".", exist_ok=True) + with tarfile.open(output, "w:gz") as tar: + add(tar, entry_name, b"traversal-test\n") + # Stand-in for a conda-pack activate script so an unpatched server + # accepts the model end-to-end rather than failing on the missing + # entry-point. + add(tar, "bin/activate", b"#!/bin/bash\n", mode=0o755) + + print(f"[zipslip] wrote {output} (mode={mode}, entry={entry_name!r})") + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + ap.add_argument("--mode", required=True, choices=["relative", "absolute"]) + ap.add_argument( + "--output", required=True, help="path to write the malicious .tar.gz" + ) + ap.add_argument( + "--marker", + required=True, + help="absolute path under /tmp/ where the traversal " + "would land on a vulnerable server", + ) + args = ap.parse_args() + + try: + build_archive(args.mode, args.output, args.marker) + except ValueError as e: + print(f"error: {e}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 258a8cbcf7023fcd8fed9753605ac725655ecab7 Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Tue, 2 Jun 2026 07:10:44 -1000 Subject: [PATCH 223/230] post: Update default branch post-26.05 (#8804) Co-authored-by: J Wyman Co-authored-by: Yingge He <157551214+yinggeh@users.noreply.github.com> --- Dockerfile.sdk | 2 +- README.md | 10 +++++----- build.py | 2 +- deploy/aws/values.yaml | 2 +- deploy/fleetcommand/Chart.yaml | 2 +- deploy/fleetcommand/values.yaml | 6 +++--- deploy/gcp/values.yaml | 2 +- .../perf-analyzer-script/triton_client.yaml | 2 +- .../server-deployer/build_and_push.sh | 4 ++-- .../server-deployer/chart/triton/Chart.yaml | 2 +- .../server-deployer/chart/triton/values.yaml | 6 +++--- .../server-deployer/data-test/schema.yaml | 2 +- .../server-deployer/schema.yaml | 4 ++-- .../gke-marketplace-app/trt-engine/README.md | 6 +++--- deploy/k8s-onprem/values.yaml | 2 +- deploy/oci/values.yaml | 2 +- docs/customization_guide/compose.md | 18 +++++++++--------- docs/getting_started/llm.md | 4 ++-- docs/introduction/compatibility.md | 3 +++ docs/introduction/release_notes.md | 4 ++-- docs/user_guide/performance_tuning.md | 4 ++-- python/openai/README.md | 6 +++--- qa/common/gen_jetson_trt_models | 2 +- qa/common/gen_qa_model_repository | 2 +- 24 files changed, 51 insertions(+), 48 deletions(-) diff --git a/Dockerfile.sdk b/Dockerfile.sdk index 8febb7bf39..b2181abe6e 100644 --- a/Dockerfile.sdk +++ b/Dockerfile.sdk @@ -29,7 +29,7 @@ # # Base image on the minimum Triton container -ARG BASE_IMAGE=nvcr.io/nvidia/tritonserver:26.04-py3-min +ARG BASE_IMAGE=nvcr.io/nvidia/tritonserver:26.05-py3-min ARG TRITON_CLIENT_REPO_SUBDIR=clientrepo ARG TRITON_REPO_ORGANIZATION=http://github.com/triton-inference-server diff --git a/README.md b/README.md index 4db3a159c7..fdb6b2a5bf 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ >[!WARNING] >You are currently on the `main` branch which tracks under-development progress ->towards the next release. The current release is version [2.68.0](https://github.com/triton-inference-server/server/releases/latest) ->and corresponds to the 26.04 container release on NVIDIA GPU Cloud (NGC). +>towards the next release. The current release is version [2.69.0](https://github.com/triton-inference-server/server/releases/latest) +>and corresponds to the 26.05 container release on NVIDIA GPU Cloud (NGC). # Triton Inference Server @@ -90,16 +90,16 @@ Inference Server with the ```bash # Step 1: Create the example model repository -git clone -b r26.04 https://github.com/triton-inference-server/server.git +git clone -b r26.05 https://github.com/triton-inference-server/server.git cd server/docs/examples ./fetch_models.sh # Step 2: Launch triton from the NGC Triton container -docker run --gpus=1 --rm --net=host -v ${PWD}/model_repository:/models nvcr.io/nvidia/tritonserver:26.04-py3 tritonserver --model-repository=/models --model-control-mode explicit --load-model densenet_onnx +docker run --gpus=1 --rm --net=host -v ${PWD}/model_repository:/models nvcr.io/nvidia/tritonserver:26.05-py3 tritonserver --model-repository=/models --model-control-mode explicit --load-model densenet_onnx # Step 3: Sending an Inference Request # In a separate console, launch the image_client example from the NGC Triton SDK container -docker run -it --rm --net=host nvcr.io/nvidia/tritonserver:26.04-py3-sdk /workspace/install/bin/image_client -m densenet_onnx -c 3 -s INCEPTION /workspace/images/mug.jpg +docker run -it --rm --net=host nvcr.io/nvidia/tritonserver:26.05-py3-sdk /workspace/install/bin/image_client -m densenet_onnx -c 3 -s INCEPTION /workspace/images/mug.jpg # Inference should return the following Image '/workspace/images/mug.jpg': diff --git a/build.py b/build.py index 9baaedbfe3..aa189459de 100755 --- a/build.py +++ b/build.py @@ -73,7 +73,7 @@ DEFAULT_TRITON_VERSION_MAP = { "release_version": "2.70.0dev", "triton_container_version": "26.06dev", - "upstream_container_version": "26.04", + "upstream_container_version": "26.05", "ort_version": "1.24.4", "ort_openvino_version": "2026.1.0", "standalone_openvino_version": "2026.1.0", diff --git a/deploy/aws/values.yaml b/deploy/aws/values.yaml index a140611d4f..c94f832aa8 100644 --- a/deploy/aws/values.yaml +++ b/deploy/aws/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:26.04-py3 + imageName: nvcr.io/nvidia/tritonserver:26.05-py3 pullPolicy: IfNotPresent modelRepositoryPath: s3://triton-inference-server-repository/model_repository numGpus: 1 diff --git a/deploy/fleetcommand/Chart.yaml b/deploy/fleetcommand/Chart.yaml index e96abde6f5..bd360e7955 100644 --- a/deploy/fleetcommand/Chart.yaml +++ b/deploy/fleetcommand/Chart.yaml @@ -26,7 +26,7 @@ apiVersion: v1 # appVersion is the Triton version; update when changing release -appVersion: 2.68.0 +appVersion: 2.69.0 description: Triton Inference Server (Fleet Command) name: triton-inference-server # version is the Chart version; update when changing anything in the chart diff --git a/deploy/fleetcommand/values.yaml b/deploy/fleetcommand/values.yaml index 6ecf3b351d..b911db4afd 100644 --- a/deploy/fleetcommand/values.yaml +++ b/deploy/fleetcommand/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:26.04-py3 + imageName: nvcr.io/nvidia/tritonserver:26.05-py3 pullPolicy: IfNotPresent numGpus: 1 serverCommand: tritonserver @@ -47,13 +47,13 @@ image: # # To set model control mode, uncomment and configure below # TODO: Fix the following url, it is invalid - # See https://github.com/triton-inference-server/server/blob/r26.04/docs/user_guide/model_management.md + # See https://github.com/triton-inference-server/server/blob/r26.05/docs/user_guide/model_management.md # for more details #- --model-control-mode=explicit|poll|none # # Additional server args # - # see https://github.com/triton-inference-server/server/blob/r26.04/README.md + # see https://github.com/triton-inference-server/server/blob/r26.05/README.md # for more details service: diff --git a/deploy/gcp/values.yaml b/deploy/gcp/values.yaml index c9900d68a0..9784c9d252 100644 --- a/deploy/gcp/values.yaml +++ b/deploy/gcp/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:26.04-py3 + imageName: nvcr.io/nvidia/tritonserver:26.05-py3 pullPolicy: IfNotPresent modelRepositoryPath: gs://triton-inference-server-repository/model_repository numGpus: 1 diff --git a/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml b/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml index a732a1da20..0e1347f4fd 100644 --- a/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml +++ b/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml @@ -33,7 +33,7 @@ metadata: namespace: default spec: containers: - - image: nvcr.io/nvidia/tritonserver:26.04-py3-sdk + - image: nvcr.io/nvidia/tritonserver:26.05-py3-sdk imagePullPolicy: Always name: nv-triton-client securityContext: diff --git a/deploy/gke-marketplace-app/server-deployer/build_and_push.sh b/deploy/gke-marketplace-app/server-deployer/build_and_push.sh index 8e00967f88..4b4468d89d 100755 --- a/deploy/gke-marketplace-app/server-deployer/build_and_push.sh +++ b/deploy/gke-marketplace-app/server-deployer/build_and_push.sh @@ -28,8 +28,8 @@ export REGISTRY=gcr.io/$(gcloud config get-value project | tr ':' '/') export APP_NAME=tritonserver export MAJOR_VERSION=2.67 -export MINOR_VERSION=2.68.0 -export NGC_VERSION=26.04-py3 +export MINOR_VERSION=2.69.0 +export NGC_VERSION=26.05-py3 docker pull nvcr.io/nvidia/$APP_NAME:$NGC_VERSION diff --git a/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml b/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml index 18f83cca68..d150f0e8d7 100644 --- a/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml +++ b/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml @@ -28,4 +28,4 @@ apiVersion: v1 appVersion: "2.68" description: Triton Inference Server name: triton-inference-server -version: 2.68.0 +version: 2.69.0 diff --git a/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml b/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml index 8cfd8171b8..362107e71a 100644 --- a/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml +++ b/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml @@ -31,14 +31,14 @@ maxReplicaCount: 3 tritonProtocol: HTTP # HPA GPU utilization autoscaling target HPATargetAverageValue: 85 -modelRepositoryPath: gs://triton_sample_models/26.04 -publishedVersion: '2.68.0' +modelRepositoryPath: gs://triton_sample_models/26.05 +publishedVersion: '2.69.0' gcpMarketplace: true image: registry: gcr.io repository: nvidia-ngc-public/tritonserver - tag: 26.04-py3 + tag: 26.05-py3 pullPolicy: IfNotPresent # modify the model repository here to match your GCP storage bucket numGpus: 1 diff --git a/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml b/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml index 7583068bc6..4c312c9880 100644 --- a/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml +++ b/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml @@ -27,7 +27,7 @@ x-google-marketplace: schemaVersion: v2 applicationApiVersion: v1beta1 - publishedVersion: '2.68.0' + publishedVersion: '2.69.0' publishedVersionMetadata: releaseNote: >- Initial release. diff --git a/deploy/gke-marketplace-app/server-deployer/schema.yaml b/deploy/gke-marketplace-app/server-deployer/schema.yaml index 457e13d19d..ccf3b157c4 100644 --- a/deploy/gke-marketplace-app/server-deployer/schema.yaml +++ b/deploy/gke-marketplace-app/server-deployer/schema.yaml @@ -27,7 +27,7 @@ x-google-marketplace: schemaVersion: v2 applicationApiVersion: v1beta1 - publishedVersion: '2.68.0' + publishedVersion: '2.69.0' publishedVersionMetadata: releaseNote: >- Initial release. @@ -89,7 +89,7 @@ properties: modelRepositoryPath: type: string title: Bucket where models are stored. Please make sure the user/service account to create the GKE app has permission to this GCS bucket. Read Triton documentation on configs and formatting details, supporting TensorRT, TensorFlow, Pytorch, Onnx ... etc. - default: gs://triton_sample_models/26.04 + default: gs://triton_sample_models/26.05 image.ldPreloadPath: type: string title: Leave this empty by default. Triton allows users to create custom layers for backend such as TensorRT plugin, the compiled shared library must be provided via LD_PRELOAD environment variable. diff --git a/deploy/gke-marketplace-app/trt-engine/README.md b/deploy/gke-marketplace-app/trt-engine/README.md index 0200987e6f..fff7466da4 100644 --- a/deploy/gke-marketplace-app/trt-engine/README.md +++ b/deploy/gke-marketplace-app/trt-engine/README.md @@ -33,7 +33,7 @@ ``` docker run --gpus all -it --network host \ --shm-size=1g --ulimit memlock=-1 --ulimit stack=67108864 \ - -v ~:/scripts nvcr.io/nvidia/tensorrt:26.04-py3 + -v ~:/scripts nvcr.io/nvidia/tensorrt:26.05-py3 pip install onnx six torch tf2onnx tensorflow @@ -57,7 +57,7 @@ mkdir -p engines python3 builder.py -m models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/model.ckpt -o engines/bert_large_int8_bs1_s128.engine -b 1 -s 128 -c models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/ -v models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_128_v19.03.1/vocab.txt --int8 --fp16 --strict --calib-num 1 -iln -imh -gsutil cp bert_large_int8_bs1_s128.engine gs://triton_sample_models/26.04/bert/1/model.plan +gsutil cp bert_large_int8_bs1_s128.engine gs://triton_sample_models/26.05/bert/1/model.plan ``` -For each Triton upgrade, container version used to generate the model, and the model path in GCS `gs://triton_sample_models/26.04/` should be updated accordingly with the correct version. +For each Triton upgrade, container version used to generate the model, and the model path in GCS `gs://triton_sample_models/26.05/` should be updated accordingly with the correct version. diff --git a/deploy/k8s-onprem/values.yaml b/deploy/k8s-onprem/values.yaml index 4dc4bf2c15..3d788f3f17 100644 --- a/deploy/k8s-onprem/values.yaml +++ b/deploy/k8s-onprem/values.yaml @@ -30,7 +30,7 @@ tags: openshift: false image: - imageName: nvcr.io/nvidia/tritonserver:26.04-py3 + imageName: nvcr.io/nvidia/tritonserver:26.05-py3 pullPolicy: IfNotPresent modelRepositoryServer: < Replace with the IP Address of your file server > modelRepositoryPath: /srv/models diff --git a/deploy/oci/values.yaml b/deploy/oci/values.yaml index f8867069c1..df5d60066d 100644 --- a/deploy/oci/values.yaml +++ b/deploy/oci/values.yaml @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:26.04-py3 + imageName: nvcr.io/nvidia/tritonserver:26.05-py3 pullPolicy: IfNotPresent modelRepositoryPath: s3://https://.compat.objectstorage..oraclecloud.com:443/triton-inference-server-repository numGpus: 1 diff --git a/docs/customization_guide/compose.md b/docs/customization_guide/compose.md index e88f0c90ba..e922d27fbe 100644 --- a/docs/customization_guide/compose.md +++ b/docs/customization_guide/compose.md @@ -46,8 +46,8 @@ The `compose.py` script can be found in the Simply clone the repository and run `compose.py` to create a custom container. Note: Created container version will depend on the branch that was cloned. For example branch - [r26.04](https://github.com/triton-inference-server/server/tree/r26.04) -should be used to create a image based on the NGC 26.04 Triton release. + [r26.05](https://github.com/triton-inference-server/server/tree/r26.05) +should be used to create a image based on the NGC 26.05 Triton release. `compose.py` provides `--backend`, `--repoagent` options that allow you to specify which backends and repository agents to include in the custom image. @@ -78,20 +78,20 @@ For example, running ``` python3 compose.py --backend pytorch --repoagent checksum ``` -on branch [r26.04](https://github.com/triton-inference-server/server/tree/r26.04) pulls: -- `min` container `nvcr.io/nvidia/tritonserver:26.04-py3-min` -- `full` container `nvcr.io/nvidia/tritonserver:26.04-py3` +on branch [r26.05](https://github.com/triton-inference-server/server/tree/r26.05) pulls: +- `min` container `nvcr.io/nvidia/tritonserver:26.05-py3-min` +- `full` container `nvcr.io/nvidia/tritonserver:26.05-py3` Alternatively, users can specify the version of Triton container to pull from any branch by either: 1. Adding flag `--container-version ` to branch ``` -python3 compose.py --backend pytorch --repoagent checksum --container-version 26.04 +python3 compose.py --backend pytorch --repoagent checksum --container-version 26.05 ``` 2. Specifying `--image min, --image full,`. The user is responsible for specifying compatible `min` and `full` containers. ``` -python3 compose.py --backend pytorch --repoagent checksum --image min,nvcr.io/nvidia/tritonserver:26.04-py3-min --image full,nvcr.io/nvidia/tritonserver:26.04-py3 +python3 compose.py --backend pytorch --repoagent checksum --image min,nvcr.io/nvidia/tritonserver:26.05-py3-min --image full,nvcr.io/nvidia/tritonserver:26.05-py3 ``` Method 1 and 2 will result in the same composed container. Furthermore, `--image` flag overrides the `--container-version` flag when both are specified. @@ -102,8 +102,8 @@ Note: 2. vLLM and TensorRT-LLM backends are currently not supported backends for `compose.py`. If you want to build additional backends on top of these backends, it would be better to [build it yourself](#build-it-yourself) by using -`nvcr.io/nvidia/tritonserver:26.04-vllm-python-py3` or -`nvcr.io/nvidia/tritonserver:26.04-trtllm-python-py3` as a `min` container. +`nvcr.io/nvidia/tritonserver:26.05-vllm-python-py3` or +`nvcr.io/nvidia/tritonserver:26.05-trtllm-python-py3` as a `min` container. ### CPU-only container composition diff --git a/docs/getting_started/llm.md b/docs/getting_started/llm.md index 052d7829ca..6ea9a5aa33 100644 --- a/docs/getting_started/llm.md +++ b/docs/getting_started/llm.md @@ -282,7 +282,7 @@ The above needs to be done manually with your favorite editor. Once finished, pl -v $(pwd)/all_models:/opt/all_models \ -v $(pwd)/scripts:/opt/scripts \ -v $(pwd)/Phi-3-mini-4k-instruct:/opt/Phi-3-mini-4k-instruct \ - nvcr.io/nvidia/tritonserver:26.04-trtllm-python-py3 + nvcr.io/nvidia/tritonserver:26.05-trtllm-python-py3 # Launch Server python3 ../scripts/launch_triton_server.py --model_repo ../all_models/inflight_batcher_llm --world_size 1 @@ -308,7 +308,7 @@ The above needs to be done manually with your favorite editor. Once finished, pl - export RELEASE="26.04" + export RELEASE="26.05" docker run -it --net=host --gpus '"device=0"' nvcr.io/nvidia/tritonserver:${RELEASE}-py3-sdk 17. ## Download the Phi-3 tokenizer diff --git a/docs/introduction/compatibility.md b/docs/introduction/compatibility.md index 63398d3d08..5fdb71adee 100644 --- a/docs/introduction/compatibility.md +++ b/docs/introduction/compatibility.md @@ -38,6 +38,7 @@ | Triton release version | NGC Tag | Python version | Torch version | TensorRT version | TensorRT-LLM version | CUDA version | CUDA Driver version | Size | | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 26.05 | nvcr.io/nvidia/tritonserver:26.05-trtllm-python-py3 | Python 3.12.3 | 2.10.0a0+b4e4ee81d3.nv25.12 | 10.14.1.48 | 1.2.1 | 13.1.0.036 | 590.44.01 | 14.22 GB | | 26.04 | nvcr.io/nvidia/tritonserver:26.04-trtllm-python-py3 | Python 3.12.3 | 2.10.0a0+b4e4ee81d3.nv25.12 | 10.14.1.48 | 1.2.1 | 13.1.0.036 | 590.44.01 | 14.22 GB | | 26.03 | nvcr.io/nvidia/tritonserver:26.03-trtllm-python-py3 | Python 3.12.3 | 2.10.0a0+b4e4ee81d3.nv25.12 | 10.14.1.48 | 1.2.0 | 13.1.0.036 | 590.44.01 | 14.18 GB | | 26.02 | nvcr.io/nvidia/tritonserver:26.02-trtllm-python-py3 | Python 3.12.3 | 2.9.0a0+145a3a7bda.nv25.10 | 10.13.3.9 | 1.1.0 | 13.0.2.006 | 580.95.05 | 16.17 GB | @@ -68,6 +69,7 @@ | Triton release version | NGC Tag | Python version | vLLM version | CUDA version | CUDA Driver version | Size | | --- | --- | --- | --- | --- | --- | --- | +| 26.05 | nvcr.io/nvidia/tritonserver:26.05-vllm-python-py3 | Python 3.12.3 | 0.19.0+6bc3197f.nv26.04.48761268 | 13.2.1.009 | 595.58.03 | 9.3G | | 26.04 | nvcr.io/nvidia/tritonserver:26.04-vllm-python-py3 | Python 3.12.3 | 0.19.0+6bc3197f.nv26.04.48761268 | 13.2.1.009 | 595.58.03 | 9.09G | | 26.03 | nvcr.io/nvidia/tritonserver:26.03-vllm-python-py3 | Python 3.12.3 | 0.17.1+fb2e3ab6.nv26.3.46332470.cu132 | 13.2.0.046 | 595.45.04 | 9.22G | | 26.02 | nvcr.io/nvidia/tritonserver:26.02-vllm-python-py3 | Python 3.12.3 | 0.15.1+nv26.2 | 13.1.1.006 | 590.48.01 | 8.9G | @@ -98,6 +100,7 @@ | Triton release version | ONNX Runtime | | --- | --- | +| 26.05 | 1.24.4 | | 26.04 | 1.24.4 | | 26.03 | 1.24.2 | | 26.02 | 1.24.1 | diff --git a/docs/introduction/release_notes.md b/docs/introduction/release_notes.md index 295734b89f..19fc0f22f3 100644 --- a/docs/introduction/release_notes.md +++ b/docs/introduction/release_notes.md @@ -25,9 +25,9 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --> -# [Triton Inference Server Release 26.04](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-26-04.html#rel-26-04) +# [Triton Inference Server Release 26.05](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-26-05.html#rel-26-05) -The Triton Inference Server container image, release 26.04, is available +The Triton Inference Server container image, release 26.05, is available on [NGC](https://ngc.nvidia.com/catalog/containers/nvidia:tritonserver) and is open source on [GitHub](https://github.com/triton-inference-server/server). Release notes can diff --git a/docs/user_guide/performance_tuning.md b/docs/user_guide/performance_tuning.md index d51ad5ab2f..ff837a4629 100644 --- a/docs/user_guide/performance_tuning.md +++ b/docs/user_guide/performance_tuning.md @@ -235,7 +235,7 @@ with a `tritonserver` binary. ```bash # Start server container -docker run -ti --rm --gpus=all --network=host -v $PWD:/mnt --name triton-server nvcr.io/nvidia/tritonserver:26.04-py3 +docker run -ti --rm --gpus=all --network=host -v $PWD:/mnt --name triton-server nvcr.io/nvidia/tritonserver:26.05-py3 # Start serving your models tritonserver --model-repository=/mnt/models @@ -284,7 +284,7 @@ by setting the `-u` flag, such as `perf_analyzer -m densenet_onnx -u ```bash # Start the SDK container interactively -docker run -ti --rm --gpus=all --network=host -v $PWD:/mnt --name triton-client nvcr.io/nvidia/tritonserver:26.04-py3-sdk +docker run -ti --rm --gpus=all --network=host -v $PWD:/mnt --name triton-client nvcr.io/nvidia/tritonserver:26.05-py3-sdk # Benchmark model being served from step 3 perf_analyzer -m densenet_onnx --concurrency-range 1:4 diff --git a/python/openai/README.md b/python/openai/README.md index d32ce97356..92d91d4db7 100644 --- a/python/openai/README.md +++ b/python/openai/README.md @@ -46,7 +46,7 @@ docker run -it --net=host --gpus all --rm \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ -e HF_TOKEN \ - nvcr.io/nvidia/tritonserver:26.04-vllm-python-py3 + nvcr.io/nvidia/tritonserver:26.05-vllm-python-py3 ``` 2. Launch the OpenAI-compatible Triton Inference Server: @@ -355,7 +355,7 @@ Currently, OpenAI-Compatible Frontend supports loading embedding models and embe docker run -it --net=host --gpus all --rm \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ -e HF_TOKEN \ - nvcr.io/nvidia/tritonserver:26.04-vllm-python-py3 + nvcr.io/nvidia/tritonserver:26.05-vllm-python-py3 ``` 2. Launch the OpenAI-compatible Triton Inference Server: @@ -451,7 +451,7 @@ docker run -it --net=host --gpus all --rm \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ -e HF_TOKEN \ -e TRTLLM_ORCHESTRATOR=1 \ - nvcr.io/nvidia/tritonserver:26.04-trtllm-python-py3 + nvcr.io/nvidia/tritonserver:26.05-trtllm-python-py3 ``` 2. Install dependencies inside the container: diff --git a/qa/common/gen_jetson_trt_models b/qa/common/gen_jetson_trt_models index 7fcdb49823..4d491fa2a1 100755 --- a/qa/common/gen_jetson_trt_models +++ b/qa/common/gen_jetson_trt_models @@ -34,7 +34,7 @@ # Make all generated files accessible outside of container umask 0000 # Set the version of the models -TRITON_VERSION=${TRITON_VERSION:=26.04} +TRITON_VERSION=${TRITON_VERSION:=26.05} # Set the CUDA device to use NVIDIA_VISIBLE_DEVICES=${NVIDIA_VISIBLE_DEVICES:=0} # Set TensorRT image diff --git a/qa/common/gen_qa_model_repository b/qa/common/gen_qa_model_repository index 8841060116..1038897618 100755 --- a/qa/common/gen_qa_model_repository +++ b/qa/common/gen_qa_model_repository @@ -66,7 +66,7 @@ log_message.status "Changing working directory to the script directory to: " "${ cd ${TRITON_MDLS_BASE_SCRIPT_DIR} log_message.status "define: default values" -TRITON_VERSION=${TRITON_VERSION:=26.04} +TRITON_VERSION=${TRITON_VERSION:=26.05} ONNX_VERSION=1.20.1 ONNX_OPSET=0 OPENVINO_VERSION=2024.5.0 From 632885393679bc35053c043a471e290055f23c86 Mon Sep 17 00:00:00 2001 From: Vinya567 Date: Tue, 2 Jun 2026 12:44:58 -0700 Subject: [PATCH 224/230] Remove Windows server build support (#8812) --- CMakeLists.txt | 21 +- build.py | 548 +++++++++++----------------------------- src/CMakeLists.txt | 432 ++++++++++++------------------- src/grpc/CMakeLists.txt | 25 +- 4 files changed, 320 insertions(+), 706 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 68f68e1a69..bd91a52109 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2020-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -166,22 +166,9 @@ if (OPENSSL_ROOT_DIR) set(_CMAKE_ARGS_OPENSSL_ROOT_DIR "-DOPENSSL_ROOT_DIR:PATH=${OPENSSL_ROOT_DIR}") endif() -# Location where protobuf-config.cmake will be installed varies by -# platform -if (WIN32) - set(_FINDPACKAGE_PROTOBUF_CONFIG_DIR "${TRITON_THIRD_PARTY_INSTALL_PREFIX}/protobuf/cmake") -else() - set(_FINDPACKAGE_PROTOBUF_CONFIG_DIR "${TRITON_THIRD_PARTY_INSTALL_PREFIX}/protobuf/${LIB_DIR}/cmake/protobuf") -endif() +set(_FINDPACKAGE_PROTOBUF_CONFIG_DIR "${TRITON_THIRD_PARTY_INSTALL_PREFIX}/protobuf/${LIB_DIR}/cmake/protobuf") -# Triton with Opentelemetry is not supported on Windows -# FIXME: add location for Windows, when support is added -# JIRA DLIS-4786 -if (WIN32) - set(_FINDPACKAGE_OPENTELEMETRY_CONFIG_DIR "") -else() - set(_FINDPACKAGE_OPENTELEMETRY_CONFIG_DIR "${TRITON_THIRD_PARTY_INSTALL_PREFIX}/opentelemetry-cpp/${LIB_DIR}/cmake/opentelemetry-cpp") -endif() +set(_FINDPACKAGE_OPENTELEMETRY_CONFIG_DIR "${TRITON_THIRD_PARTY_INSTALL_PREFIX}/opentelemetry-cpp/${LIB_DIR}/cmake/opentelemetry-cpp") if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(TRITON_INSTALL_PREFIX ${CMAKE_CURRENT_BINARY_DIR}/install) @@ -202,7 +189,7 @@ endif() # TRITON_ENABLE_HTTP || TRITON_ENABLE_METRICS || TRITON_ENABLE_SAGEMAKER if(${TRITON_ENABLE_GRPC}) set(TRITON_DEPENDS ${TRITON_DEPENDS} grpc) endif() # TRITON_ENABLE_GRPC -if(NOT WIN32 AND ${TRITON_ENABLE_TRACING}) +if(${TRITON_ENABLE_TRACING}) set(TRITON_DEPENDS ${TRITON_DEPENDS} opentelemetry-cpp) endif() # TRITON_ENABLE_TRACING diff --git a/build.py b/build.py index aa189459de..8f034d5234 100755 --- a/build.py +++ b/build.py @@ -118,6 +118,8 @@ def fail_if(p, msg): def target_platform(): # When called by compose.py, FLAGS will be None if FLAGS and FLAGS.target_platform is not None: + if FLAGS.target_platform == "windows": + fail("Windows is no longer a supported target platform.") return FLAGS.target_platform platform_string = platform.system().lower() if platform_string == "linux": @@ -128,6 +130,8 @@ def target_platform(): return "linux" else: return "rhel" + elif platform_string == "windows": + fail("Windows is no longer a supported target platform.") else: return platform_string @@ -167,13 +171,6 @@ def __del__(self): def close(self): if self._file is not None: - if target_platform() == "windows": - self.blankln() - self._file.write("}\n") - self._file.write("catch {\n") - self._file.write(" $_;\n") - self._file.write(" ExitWithCode 1;\n") - self._file.write("}\n") """Close the file""" self._file.close() self._file = None @@ -201,8 +198,7 @@ def comment_verbose(self, msg=""): self.comment(msg) def header(self, desc=None): - if target_platform() != "windows": - self._file.write("#!/usr/bin/env bash\n\n") + self._file.write("#!/usr/bin/env bash\n\n") if desc is not None: self.comment() @@ -211,27 +207,12 @@ def header(self, desc=None): self.blankln() self.comment("Exit script immediately if any command fails") - if target_platform() == "windows": - self._file.write("$UseStructuredOutput = $false\n") - self.blankln() - self._file.write("function ExitWithCode($exitcode) {\n") - self._file.write(" $host.SetShouldExit($exitcode)\n") - self._file.write(" exit $exitcode\n") - self._file.write("}\n") - self.blankln() - if self._verbose: - self._file.write("Set-PSDebug -Trace 1\n") - self.blankln() - self._file.write("try {\n") - else: - self._file.write("set -e\n") - if self._verbose: - self._file.write("set -x\n") + self._file.write("set -e\n") + if self._verbose: + self._file.write("set -x\n") self.blankln() def envvar_ref(self, v): - if target_platform() == "windows": - return f"${{env:{v}}}" return f"${{{v}}}" def cmd(self, clist, check_exitcode=False): @@ -242,54 +223,23 @@ def cmd(self, clist, check_exitcode=False): self._file.write(f"{c} ") self.blankln() - if check_exitcode: - if target_platform() == "windows": - self._file.write("if ($LASTEXITCODE -ne 0) {\n") - self._file.write( - ' Write-Output "exited with status code $LASTEXITCODE";\n' - ) - self._file.write(" ExitWithCode 1;\n") - self._file.write("}\n") - def cwd(self, path): - if target_platform() == "windows": - self.cmd(f"Set-Location -EV Err -EA Stop {path}") - else: - self.cmd(f"cd {path}") + self.cmd(f"cd {path}") def cp(self, src, dest): - if target_platform() == "windows": - self.cmd(f"Copy-Item -EV Err -EA Stop {src} -Destination {dest}") - else: - self.cmd(f"cp {src} {dest}") + self.cmd(f"cp {src} {dest}") def mkdir(self, path): - if target_platform() == "windows": - self.cmd( - f"New-Item -EV Err -EA Stop -ItemType Directory -Force -Path {path}" - ) - else: - self.cmd(f"mkdir -p {pathlib.Path(path)}") + self.cmd(f"mkdir -p {pathlib.Path(path)}") def rmdir(self, path): - if target_platform() == "windows": - self.cmd(f"if (Test-Path -Path {path}) {{") - self.cmd(f" Remove-Item -EV Err -EA Stop -Recurse -Force {path}") - self.cmd("}") - else: - self.cmd(f"rm -fr {pathlib.Path(path)}") + self.cmd(f"rm -fr {pathlib.Path(path)}") def cpdir(self, src, dest): - if target_platform() == "windows": - self.cmd(f"Copy-Item -EV Err -EA Stop -Recurse {src} -Destination {dest}") - else: - self.cmd(f"cp -r {src} {dest}") + self.cmd(f"cp -r {src} {dest}") def tar(self, subdir, tar_filename): - if target_platform() == "windows": - fail("unsupported operation: tar") - else: - self.cmd(f"tar zcf {tar_filename} {subdir}") + self.cmd(f"tar zcf {tar_filename} {subdir}") def cmake(self, args): # Pass some additional envvars into cmake... @@ -309,10 +259,7 @@ def gitclone(self, repo, tag, subdir, org): if not FLAGS.no_force_clone: self.rmdir(clone_dir) - if target_platform() == "windows": - self.cmd(f"if (-Not (Test-Path -Path {clone_dir})) {{") - else: - self.cmd(f"if [[ ! -e {clone_dir} ]]; then") + self.cmd(f"if [[ ! -e {clone_dir} ]]; then") # FIXME [DLIS-4045 - Currently the tag starting with "pull/" is not # working with "--repo-tag" as the option is not forwarded to the @@ -325,7 +272,7 @@ def gitclone(self, repo, tag, subdir, org): f" git clone --recursive --depth=1 {org}/{repo}.git {subdir}; git --git-dir {subdir}/.git log --oneline -1", check_exitcode=True, ) - self.cmd("}" if target_platform() == "windows" else "fi") + self.cmd("fi") self.cwd(subdir) self.cmd(f"git fetch origin {tag}:tritonbuildref", check_exitcode=True) self.cmd(f"git checkout tritonbuildref", check_exitcode=True) @@ -334,7 +281,7 @@ def gitclone(self, repo, tag, subdir, org): f" git clone --recursive --single-branch --depth=1 -b {tag} {org}/{repo}.git {subdir}; git --git-dir {subdir}/.git log --oneline -1", check_exitcode=True, ) - self.cmd("}" if target_platform() == "windows" else "fi") + self.cmd("fi") def cmake_core_arg(name, type, value): @@ -604,14 +551,7 @@ def backend_cmake_args(images, components, be, install_dir, library_paths): cmake_backend_enable(be, "TRITON_ENABLE_METRICS", FLAGS.enable_metrics) ) - # [DLIS-4950] always enable below once Windows image is updated with CUPTI - # cargs.append(cmake_backend_enable(be, 'TRITON_ENABLE_MEMORY_TRACKER', True)) - if (target_platform() == "windows") and (not FLAGS.no_container_build): - print( - "Warning: Detected docker build is used for Windows, backend utility 'device memory tracker' will be disabled due to missing library in CUDA Windows docker image." - ) - cargs.append(cmake_backend_enable(be, "TRITON_ENABLE_MEMORY_TRACKER", False)) - elif target_platform() == "igpu": + if target_platform() == "igpu": print( "Warning: Detected iGPU build, backend utility 'device memory tracker' will be disabled as iGPU doesn't contain required version of the library." ) @@ -690,59 +630,51 @@ def onnxruntime_cmake_args(images, library_paths): ) ) - if target_platform() == "windows": - if "base" in images: - cargs.append( - cmake_backend_arg( - "onnxruntime", "TRITON_BUILD_CONTAINER", None, images["base"] - ) + if "base" in images: + cargs.append( + cmake_backend_arg( + "onnxruntime", "TRITON_BUILD_CONTAINER", None, images["base"] ) + ) else: - if "base" in images: - cargs.append( - cmake_backend_arg( - "onnxruntime", "TRITON_BUILD_CONTAINER", None, images["base"] - ) - ) - else: - cargs.append( - cmake_backend_arg( - "onnxruntime", - "TRITON_BUILD_CONTAINER_VERSION", - None, - FLAGS.upstream_container_version, - ) + cargs.append( + cmake_backend_arg( + "onnxruntime", + "TRITON_BUILD_CONTAINER_VERSION", + None, + FLAGS.upstream_container_version, ) + ) - # TODO: TPRD-333 OpenVino extension is not currently supported by our manylinux build - if ( - (target_machine() != "aarch64") - and (target_platform() != "rhel") - and (FLAGS.ort_openvino_version is not None) - ): - cargs.append( - cmake_backend_enable( - "onnxruntime", "TRITON_ENABLE_ONNXRUNTIME_OPENVINO", True - ) + # TODO: TPRD-333 OpenVino extension is not currently supported by our manylinux build + if ( + (target_machine() != "aarch64") + and (target_platform() != "rhel") + and (FLAGS.ort_openvino_version is not None) + ): + cargs.append( + cmake_backend_enable( + "onnxruntime", "TRITON_ENABLE_ONNXRUNTIME_OPENVINO", True ) - cargs.append( - cmake_backend_arg( - "onnxruntime", - "TRITON_BUILD_ONNXRUNTIME_OPENVINO_VERSION", - None, - FLAGS.ort_openvino_version, - ) + ) + cargs.append( + cmake_backend_arg( + "onnxruntime", + "TRITON_BUILD_ONNXRUNTIME_OPENVINO_VERSION", + None, + FLAGS.ort_openvino_version, ) + ) - if (target_platform() == "igpu") or (target_platform() == "rhel"): - cargs.append( - cmake_backend_arg( - "onnxruntime", - "TRITON_BUILD_TARGET_PLATFORM", - None, - target_platform(), - ) + if (target_platform() == "igpu") or (target_platform() == "rhel"): + cargs.append( + cmake_backend_arg( + "onnxruntime", + "TRITON_BUILD_TARGET_PLATFORM", + None, + target_platform(), ) + ) return cargs @@ -756,29 +688,21 @@ def openvino_cmake_args(): FLAGS.standalone_openvino_version, ) ] - if target_platform() == "windows": - if "base" in images: - cargs.append( - cmake_backend_arg( - "openvino", "TRITON_BUILD_CONTAINER", None, images["base"] - ) + if "base" in images: + cargs.append( + cmake_backend_arg( + "openvino", "TRITON_BUILD_CONTAINER", None, images["base"] ) + ) else: - if "base" in images: - cargs.append( - cmake_backend_arg( - "openvino", "TRITON_BUILD_CONTAINER", None, images["base"] - ) - ) - else: - cargs.append( - cmake_backend_arg( - "openvino", - "TRITON_BUILD_CONTAINER_VERSION", - None, - FLAGS.upstream_container_version, - ) + cargs.append( + cmake_backend_arg( + "openvino", + "TRITON_BUILD_CONTAINER_VERSION", + None, + FLAGS.upstream_container_version, ) + ) return cargs @@ -786,12 +710,6 @@ def tensorrt_cmake_args(): cargs = [ cmake_backend_enable("tensorrt", "TRITON_ENABLE_NVTX", FLAGS.enable_nvtx), ] - if target_platform() == "windows": - cargs.append( - cmake_backend_arg( - "tensorrt", "TRITON_TENSORRT_INCLUDE_PATHS", None, "c:/TensorRT/include" - ) - ) return cargs @@ -1062,15 +980,7 @@ def create_dockerfile_buildbase(ddir, dockerfile_name, argmap): """.format( argmap["NVIDIA_BUILD_ID"] ) - # Install the windows- or linux-specific buildbase dependencies - if target_platform() == "windows": - df += """ -RUN python3 -m pip install build - -SHELL ["cmd", "/S", "/C"] -""" - else: - df += """ + df += """ # Ensure apt-get won't prompt for selecting options ENV DEBIAN_FRONTEND=noninteractive @@ -1132,7 +1042,7 @@ def create_dockerfile_buildbase(ddir, dockerfile_name, argmap): pybind11[global] """ - df += f""" + df += f""" # Install boost version >= 1.78 for boost::span # Current libboost-dev apt packages are < 1.78, so install from tar.gz RUN wget -O /tmp/boost.tar.gz {FLAGS.boost_url} \\ @@ -1141,8 +1051,8 @@ def create_dockerfile_buildbase(ddir, dockerfile_name, argmap): && mv /tmp/boost_1_80_0/boost /usr/include/boost """ - if FLAGS.enable_gpu: - df += install_dcgm_libraries(argmap["DCGM_VERSION"], target_machine()) + if FLAGS.enable_gpu: + df += install_dcgm_libraries(argmap["DCGM_VERSION"], target_machine()) df += """ ENV TRITON_SERVER_VERSION ${TRITON_VERSION} @@ -1166,14 +1076,7 @@ def create_dockerfile_buildbase(ddir, dockerfile_name, argmap): # Copy in the triton source. We remove existing contents first in # case the FROM container has something there already. - if target_platform() == "windows": - df += """ -WORKDIR /workspace -RUN rmdir /S/Q * || exit 0 -COPY . . -""" - else: - df += """ + df += """ WORKDIR /workspace RUN rm -fr * COPY . . @@ -1614,55 +1517,6 @@ def change_default_python_version_rhel(version): return df -def create_dockerfile_windows( - ddir, dockerfile_name, argmap, backends, repoagents, caches -): - df = """ -ARG TRITON_VERSION={} -ARG TRITON_CONTAINER_VERSION={} -ARG BASE_IMAGE={} - -############################################################################ -## Production stage: Create container with just inference server executable -############################################################################ -FROM ${{BASE_IMAGE}} - -ARG TRITON_VERSION -ARG TRITON_CONTAINER_VERSION - -ENV TRITON_SERVER_VERSION=${{TRITON_VERSION}} -ENV NVIDIA_TRITON_SERVER_VERSION=${{TRITON_CONTAINER_VERSION}} -LABEL com.nvidia.tritonserver.version="${{TRITON_SERVER_VERSION}}" - -RUN setx path "%path%;C:\\opt\\tritonserver\\bin" - -""".format( - argmap["TRITON_VERSION"], - argmap["TRITON_CONTAINER_VERSION"], - argmap["BASE_IMAGE"], - ) - df += """ -WORKDIR /opt -RUN rmdir /S/Q tritonserver || exit 0 -COPY --chown=1000:1000 build/install tritonserver - -WORKDIR /opt/tritonserver -COPY --chown=1000:1000 NVIDIA_Deep_Learning_Container_License.pdf . - -""" - df += """ -ENTRYPOINT [] -ENV NVIDIA_BUILD_ID {} -LABEL com.nvidia.build.id={} -LABEL com.nvidia.build.ref={} -""".format( - argmap["NVIDIA_BUILD_ID"], argmap["NVIDIA_BUILD_ID"], argmap["NVIDIA_BUILD_REF"] - ) - - with open(os.path.join(ddir, dockerfile_name), "w") as dfile: - dfile.write(df) - - def create_build_dockerfiles( container_build_dir, images, backends, repoagents, caches, endpoints ): @@ -1672,8 +1526,6 @@ def create_build_dockerfiles( print( "warning: RHEL is not an officially supported target and you will probably experience errors attempting to build this container." ) - elif target_platform() == "windows": - base_image = "mcr.microsoft.com/dotnet/framework/sdk:4.8" elif target_platform() == "rhel": raise KeyError("A base image must be specified when targeting RHEL") elif FLAGS.enable_gpu: @@ -1700,11 +1552,7 @@ def create_build_dockerfiles( # For CPU-only image we need to copy some cuda libraries and dependencies # since we are using PyTorch containers that are not CPU-only. - if ( - not FLAGS.enable_gpu - and ("pytorch" in backends) - and (target_platform() != "windows") - ): + if not FLAGS.enable_gpu and ("pytorch" in backends): if "gpu-base" in images: gpu_base_image = images["gpu-base"] else: @@ -1722,25 +1570,15 @@ def create_build_dockerfiles( FLAGS.build_dir, "Dockerfile.buildbase", dockerfileargmap ) - if target_platform() == "windows": - create_dockerfile_windows( - FLAGS.build_dir, - "Dockerfile", - dockerfileargmap, - backends, - repoagents, - caches, - ) - else: - create_dockerfile_linux( - FLAGS.build_dir, - "Dockerfile", - dockerfileargmap, - backends, - repoagents, - caches, - endpoints, - ) + create_dockerfile_linux( + FLAGS.build_dir, + "Dockerfile", + dockerfileargmap, + backends, + repoagents, + caches, + endpoints, + ) # Dockerfile used for the creating the CI base image. create_dockerfile_cibase(FLAGS.build_dir, "Dockerfile.cibase", dockerfileargmap) @@ -1782,14 +1620,7 @@ def create_docker_build_script(script_name, container_install_dir, container_ci_ "--pull", ] - # Windows docker runs in a VM and memory needs to be specified - # explicitly (at least for some configurations of docker). - if target_platform() == "windows": - if FLAGS.container_memory: - baseargs += ["--memory", FLAGS.container_memory] - - if target_platform() != "windows": - baseargs += ["--cache-from={}".format(k) for k in cachefrommap] + baseargs += ["--cache-from={}".format(k) for k in cachefrommap] baseargs += ["."] @@ -1822,35 +1653,24 @@ def create_docker_build_script(script_name, container_install_dir, container_ci_ if not FLAGS.no_container_interactive: runargs += ["-it"] - if target_platform() == "windows": - if FLAGS.container_memory: - runargs += ["--memory", FLAGS.container_memory] - runargs += ["-v", "\\\\.\\pipe\\docker_engine:\\\\.\\pipe\\docker_engine"] - else: - runargs += ["-v", "/var/run/docker.sock:/var/run/docker.sock"] - if FLAGS.use_user_docker_config: - if os.path.exists(FLAGS.use_user_docker_config): - runargs += [ - "-v", - os.path.expanduser( - FLAGS.use_user_docker_config + ":/root/.docker/config.json" - ), - ] + runargs += ["-v", "/var/run/docker.sock:/var/run/docker.sock"] + if FLAGS.use_user_docker_config: + if os.path.exists(FLAGS.use_user_docker_config): + runargs += [ + "-v", + os.path.expanduser( + FLAGS.use_user_docker_config + ":/root/.docker/config.json" + ), + ] runargs += ["tritonserver_buildbase"] - if target_platform() == "windows": - runargs += ["powershell.exe", "-noexit", "-File", "./cmake_build.ps1"] - else: - runargs += ["./cmake_build"] + runargs += ["./cmake_build"] # Remove existing tritonserver_builder container... - if target_platform() == "windows": - docker_script.cmd(["docker", "rm", "tritonserver_builder"]) - else: - docker_script._file.write( - 'if [ "$(docker ps -a | grep tritonserver_builder)" ]; then docker rm -f tritonserver_builder; fi\n' - ) + docker_script._file.write( + 'if [ "$(docker ps -a | grep tritonserver_builder)" ]; then docker rm -f tritonserver_builder; fi\n' + ) docker_script.cmd(runargs, check_exitcode=True) @@ -1941,21 +1761,7 @@ def core_build( ) cmake_script.makeinstall() - if target_platform() == "windows": - cmake_script.mkdir(os.path.join(install_dir, "bin")) - cmake_script.cp( - os.path.join(repo_install_dir, "bin", "tritonserver.exe"), - os.path.join(install_dir, "bin"), - ) - cmake_script.cp( - os.path.join(repo_install_dir, "bin", "tritonserver.dll"), - os.path.join(install_dir, "bin"), - ) - cmake_script.cp( - os.path.join(repo_install_dir, "lib", "tritonserver.lib"), - os.path.join(install_dir, "bin"), - ) - elif target_platform() == "rhel": + if target_platform() == "rhel": cmake_script.mkdir(os.path.join(install_dir, "bin")) cmake_script.cp( os.path.join(repo_install_dir, "bin", "tritonserver"), @@ -1998,25 +1804,22 @@ def core_build( cmake_script.cp(os.path.join(repo_dir, "LICENSE"), install_dir) cmake_script.cp(os.path.join(repo_dir, "TRITON_VERSION"), install_dir) - # If requested, package the source code for all OSS used to build - # For windows, Triton is not delivered as a container so skip for - # windows platform. - if target_platform() != "windows": - if ( - (not FLAGS.no_container_build) - and (not FLAGS.no_core_build) - and (not FLAGS.no_container_source) - ): - cmake_script.mkdir(os.path.join(install_dir, "third-party-src")) - cmake_script.cwd(repo_build_dir) - cmake_script.tar( - "third-party-src", - os.path.join(install_dir, "third-party-src", "src.tar.gz"), - ) - cmake_script.cp( - os.path.join(repo_dir, "docker", "README.third-party-src"), - os.path.join(install_dir, "third-party-src", "README"), - ) + # If requested, package the source code for all OSS used to build. + if ( + (not FLAGS.no_container_build) + and (not FLAGS.no_core_build) + and (not FLAGS.no_container_source) + ): + cmake_script.mkdir(os.path.join(install_dir, "third-party-src")) + cmake_script.cwd(repo_build_dir) + cmake_script.tar( + "third-party-src", + os.path.join(install_dir, "third-party-src", "src.tar.gz"), + ) + cmake_script.cp( + os.path.join(repo_dir, "docker", "README.third-party-src"), + os.path.join(install_dir, "third-party-src", "README"), + ) cmake_script.comment() cmake_script.comment("end Triton core library and tritonserver executable") @@ -2226,11 +2029,6 @@ def cibase_build( cmake_script.mkdir(ci_dir) - # On windows we are not yet using a CI/QA docker image for - # testing, so don't do anything... - if target_platform() == "windows": - return - # The core build produces some artifacts that are needed for CI # testing, so include those in the install. cmake_script.cpdir(os.path.join(repo_dir, "qa"), ci_dir) @@ -2259,12 +2057,9 @@ def cibase_build( cmake_script.mkdir(os.path.join(ci_dir, "backends")) for be in ("identity", "repeat", "square"): be_install_dir = os.path.join(build_dir, be, "install", "backends", be) - if target_platform() == "windows": - cmake_script.cmd(f"if (Test-Path -Path {be_install_dir}) {{") - else: - cmake_script.cmd(f"if [[ -e {be_install_dir} ]]; then") + cmake_script.cmd(f"if [[ -e {be_install_dir} ]]; then") cmake_script.cpdir(be_install_dir, os.path.join(ci_dir, "backends")) - cmake_script.cmd("}" if target_platform() == "windows" else "fi") + cmake_script.cmd("fi") # Some of the unit-test built backends are needed for CI testing cmake_script.mkdir(os.path.join(ci_dir, "tritonbuild", "tritonserver", "backends")) @@ -2277,15 +2072,12 @@ def cibase_build( "iterative_sequence", ): be_install_dir = os.path.join(repo_install_dir, "backends", be) - if target_platform() == "windows": - cmake_script.cmd(f"if (Test-Path -Path {be_install_dir}) {{") - else: - cmake_script.cmd(f"if [[ -e {be_install_dir} ]]; then") + cmake_script.cmd(f"if [[ -e {be_install_dir} ]]; then") cmake_script.cpdir( be_install_dir, os.path.join(ci_dir, "tritonbuild", "tritonserver", "backends"), ) - cmake_script.cmd("}" if target_platform() == "windows" else "fi") + cmake_script.cmd("fi") # The onnxruntime_backend build produces some artifacts that # are needed for CI testing. @@ -2328,52 +2120,32 @@ def finalize_build(cmake_script, install_dir, ci_dir): def enable_all(): - if target_platform() != "windows": - all_backends = [ - "ensemble", - "identity", - "square", - "repeat", - "onnxruntime", - "python", - "dali", - "pytorch", - "openvino", - "fil", - "tensorrt", - ] - all_repoagents = ["checksum"] - all_caches = ["local", "redis"] - all_filesystems = ["gcs", "s3", "azure_storage"] - all_endpoints = ["http", "grpc", "sagemaker", "vertex-ai"] - - FLAGS.enable_logging = True - FLAGS.enable_stats = True - FLAGS.enable_metrics = True - FLAGS.enable_gpu_metrics = True - FLAGS.enable_cpu_metrics = True - FLAGS.enable_tracing = True - FLAGS.enable_nvtx = True - FLAGS.enable_gpu = True - else: - all_backends = [ - "ensemble", - "identity", - "square", - "repeat", - "onnxruntime", - "openvino", - "tensorrt", - ] - all_repoagents = ["checksum"] - all_caches = ["local", "redis"] - all_filesystems = [] - all_endpoints = ["http", "grpc"] - - FLAGS.enable_logging = True - FLAGS.enable_stats = True - FLAGS.enable_tracing = True - FLAGS.enable_gpu = True + all_backends = [ + "ensemble", + "identity", + "square", + "repeat", + "onnxruntime", + "python", + "dali", + "pytorch", + "openvino", + "fil", + "tensorrt", + ] + all_repoagents = ["checksum"] + all_caches = ["local", "redis"] + all_filesystems = ["gcs", "s3", "azure_storage"] + all_endpoints = ["http", "grpc", "sagemaker", "vertex-ai"] + + FLAGS.enable_logging = True + FLAGS.enable_stats = True + FLAGS.enable_metrics = True + FLAGS.enable_gpu_metrics = True + FLAGS.enable_cpu_metrics = True + FLAGS.enable_tracing = True + FLAGS.enable_nvtx = True + FLAGS.enable_gpu = True requested_backends = [] for be in FLAGS.backend: @@ -2457,17 +2229,11 @@ def enable_all(): required=False, help="Do not use Docker --pull argument when building container.", ) - parser.add_argument( - "--container-memory", - default=None, - required=False, - help="Value for Docker --memory argument. Used only for windows builds.", - ) parser.add_argument( "--target-platform", required=False, default=None, - help='Target platform for build, can be "linux", "rhel", "windows" or "igpu". If not specified, build targets the current platform.', + help='Target platform for build, can be "linux", "rhel" or "igpu". If not specified, build targets the current platform.', ) parser.add_argument( "--target-machine", @@ -3021,21 +2787,12 @@ def enable_all(): script_install_dir = script_ci_dir = FLAGS.install_dir script_cmake_dir = FLAGS.cmake_dir if not FLAGS.no_container_build: - # FLAGS.tmp_dir may be specified with "\" on Windows, adjust - # to "/" for docker usage. - script_build_dir = os.path.normpath( - os.path.join(FLAGS.tmp_dir, "tritonbuild").replace("\\", "/") - ) + script_build_dir = os.path.normpath(os.path.join(FLAGS.tmp_dir, "tritonbuild")) script_install_dir = os.path.normpath(os.path.join(script_build_dir, "install")) script_ci_dir = os.path.normpath(os.path.join(script_build_dir, "ci")) - if target_platform() == "windows": - script_repo_dir = script_cmake_dir = os.path.normpath("c:/workspace") - else: - script_repo_dir = script_cmake_dir = "/workspace" + script_repo_dir = script_cmake_dir = "/workspace" script_name = "cmake_build" - if target_platform() == "windows": - script_name += ".ps1" # Write the build script that invokes cmake for the core, backends, repo-agents, and caches. pathlib.Path(FLAGS.build_dir).mkdir(parents=True, exist_ok=True) @@ -3136,8 +2893,7 @@ def enable_all(): # written to the build-dir while running the docker container # may have root ownership, so give them permissions to be # managed by all users on the host system. - if target_platform() != "windows": - finalize_build(cmake_script, script_install_dir, script_ci_dir) + finalize_build(cmake_script, script_install_dir, script_ci_dir) # If --no-container-build is not specified then we perform the # actual build within a docker container and from that create the @@ -3146,8 +2902,6 @@ def enable_all(): # the build process. if not FLAGS.no_container_build: script_name = "docker_build" - if target_platform() == "windows": - script_name += ".ps1" create_build_dockerfiles( script_build_dir, images, backends, repoagents, caches, FLAGS.endpoint @@ -3158,12 +2912,6 @@ def enable_all(): # container-based build is requested use 'docker_build' script, # otherwise build directly on this system using cmake script. if not FLAGS.dryrun: - if target_platform() == "windows": - p = subprocess.Popen( - ["powershell.exe", "-noexit", "-File", f"./{script_name}"], - cwd=FLAGS.build_dir, - ) - else: - p = subprocess.Popen([f"./{script_name}"], cwd=FLAGS.build_dir) + p = subprocess.Popen([f"./{script_name}"], cwd=FLAGS.build_dir) p.wait() fail_if(p.returncode != 0, "build failed") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9568f4280d..de46e970e4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -78,7 +78,7 @@ endif() # OpenTelemetry # -if (NOT WIN32 AND ${TRITON_ENABLE_TRACING}) +if (${TRITON_ENABLE_TRACING}) find_package(absl CONFIG REQUIRED) find_package(CURL CONFIG REQUIRED) find_package(nlohmann_json CONFIG REQUIRED) @@ -107,53 +107,21 @@ add_executable( triton_signal.h ) -# On windows a *.lib file can be generated for a exe. When creating -# tritonserver.exe if we try to create tritonserver.lib it will fail -# because there is already a trtionserver.lib for tritonserver.dll, -# this causes the build to fail. To avoid we keep the build name as -# main.exe and then for windows after installing we rename it to -# tritonserver.exe (below in the install steps). -if (NOT WIN32) - set_property(TARGET main PROPERTY OUTPUT_NAME tritonserver) -endif() +set_property(TARGET main PROPERTY OUTPUT_NAME tritonserver) target_compile_features(main PRIVATE cxx_std_${TRITON_MIN_CXX_STANDARD}) -if(WIN32) - message("Using MSVC as compiler, default target on Windows 10. " - "If the target system is not Windows 10, please update _WIN32_WINNT " - "to corresponding value.") - target_compile_options( - main - PRIVATE - /W1 /D_WIN32_WINNT=0x0A00 /EHsc /Zc:preprocessor - ) - target_compile_definitions(main - PRIVATE - NOMINMAX) - - # Dependency from common.h - find_library(B64_LIBRARY NAMES b64) - target_link_libraries( - main - PRIVATE - ${B64_LIBRARY} - ) - -else() - - target_compile_options( - main - PRIVATE - -Wall -Wextra -Wno-unused-parameter -Wno-deprecated-declarations -Werror - ) +target_compile_options( + main + PRIVATE + -Wall -Wextra -Wno-unused-parameter -Wno-deprecated-declarations -Werror +) - # Dependency from common.h - target_link_libraries( - main - PRIVATE - b64 - ) - endif() +# Dependency from common.h +target_link_libraries( + main + PRIVATE + b64 +) set(LIB_DIR "lib") if(LINUX) @@ -275,29 +243,17 @@ if(${TRITON_ENABLE_NVTX}) ) endif() # TRITON_ENABLE_NVTX -if (NOT WIN32) - target_link_libraries( - main - PRIVATE - rt - dl - ) -endif() # NOT WIN32 +target_link_libraries( + main + PRIVATE + rt + dl +) -if (NOT WIN32) - install( - TARGETS main - RUNTIME DESTINATION bin - ) -else() - # See explanation above as to why we need to rename main.exe to - # tritonserver.exe as part of the install process on windows. - install( - PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_BUILD_TYPE}/main.exe - DESTINATION bin - RENAME tritonserver.exe - ) -endif() +install( + TARGETS main + RUNTIME DESTINATION bin +) if(${TRITON_ENABLE_GRPC}) # @@ -374,19 +330,11 @@ if(${TRITON_ENABLE_HTTP} ) target_compile_features(http-endpoint-library PRIVATE cxx_std_${TRITON_MIN_CXX_STANDARD}) - if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") - target_compile_options( - http-endpoint-library - PRIVATE - /W1 /D_WIN32_WINNT=0x0A00 /EHsc /Zc:preprocessor - ) - else() - target_compile_options( - http-endpoint-library - PRIVATE - -Wall -Wextra -Wno-unused-parameter -Wno-deprecated-declarations -Wno-error=maybe-uninitialized -Werror - ) - endif() + target_compile_options( + http-endpoint-library + PRIVATE + -Wall -Wextra -Wno-unused-parameter -Wno-deprecated-declarations -Wno-error=maybe-uninitialized -Werror + ) set_target_properties( http-endpoint-library @@ -411,10 +359,7 @@ if(${TRITON_ENABLE_HTTP} PRIVATE $ ) - # FIXME when Triton support of Opentelemetry is available on Windows - # add ${OPENTELEMETRY_CPP_INCLUDE_DIRS} to above target_include_directories - # JIRA DLIS-4786 - if (NOT WIN32 AND ${TRITON_ENABLE_TRACING}) + if (${TRITON_ENABLE_TRACING}) target_link_libraries( http-endpoint-library PRIVATE tracing-library @@ -491,23 +436,12 @@ if(${TRITON_ENABLE_HTTP} ) endif() # TRITON_ENABLE_NVTX - if (WIN32) - find_library(B64_LIBRARY NAMES b64) - find_library(ZLIB_LIBRARY NAMES zlib) - target_link_libraries( - http-endpoint-library - PUBLIC - ${B64_LIBRARY} - ${ZLIB_LIBRARY} - ) - else() - target_link_libraries( - http-endpoint-library - PUBLIC - b64 - z - ) - endif() + target_link_libraries( + http-endpoint-library + PUBLIC + b64 + z + ) target_link_libraries( main @@ -528,18 +462,15 @@ if(${TRITON_ENABLE_TRACING}) ) target_compile_features(tracing-library PRIVATE cxx_std_${TRITON_MIN_CXX_STANDARD}) - # FIXME: remove, when Windows support is added for Opentelemetry - if (NOT WIN32) - target_include_directories( - tracing-library - PUBLIC ${OPENTELEMETRY_CPP_INCLUDE_DIRS} - ) + target_include_directories( + tracing-library + PUBLIC ${OPENTELEMETRY_CPP_INCLUDE_DIRS} + ) - target_link_libraries( - tracing-library - PUBLIC - ${OPENTELEMETRY_CPP_LIBRARIES}) - endif() + target_link_libraries( + tracing-library + PUBLIC + ${OPENTELEMETRY_CPP_LIBRARIES}) set_target_properties( tracing-library @@ -610,99 +541,130 @@ if(${TRITON_ENABLE_TRACING}) ) endif() # TRITON_ENABLE_TRACING -if (NOT WIN32) - # - # simple - # - add_executable( - simple - simple.cc - ) +# +# simple +# +add_executable( + simple + simple.cc +) - target_compile_features(simple PRIVATE cxx_std_${TRITON_MIN_CXX_STANDARD}) - if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") - message("Using MSVC as compiler, default target on Windows 10. " - "If the target system is not Windows 10, please update _WIN32_WINNT " - "to corresponding value.") - target_compile_options( - simple - PRIVATE - /W1 /D_WIN32_WINNT=0x0A00 /EHsc /Zc:preprocessor - ) - else() - target_compile_options( - simple - PRIVATE - -Wall -Wextra -Wno-type-limits -Wno-unused-parameter -Wno-deprecated-declarations -Werror - ) - endif() +target_compile_features(simple PRIVATE cxx_std_${TRITON_MIN_CXX_STANDARD}) +target_compile_options( + simple + PRIVATE + -Wall -Wextra -Wno-type-limits -Wno-unused-parameter -Wno-deprecated-declarations -Werror +) - set_target_properties( +set_target_properties( + simple + PROPERTIES + POSITION_INDEPENDENT_CODE ON + SKIP_BUILD_RPATH TRUE + BUILD_WITH_INSTALL_RPATH TRUE + INSTALL_RPATH_USE_LINK_PATH FALSE + INSTALL_RPATH "" +) + +target_link_libraries( + simple + PRIVATE + triton-common-async-work-queue # from repo-common + triton-common-error # from repo-common + triton-core-serverapi # from repo-core + triton-core-serverstub # from repo-core + ) + +if(${TRITON_ENABLE_GPU}) + target_compile_definitions( simple - PROPERTIES - POSITION_INDEPENDENT_CODE ON - SKIP_BUILD_RPATH TRUE - BUILD_WITH_INSTALL_RPATH TRUE - INSTALL_RPATH_USE_LINK_PATH FALSE - INSTALL_RPATH "" + PRIVATE TRITON_ENABLE_GPU=1 + PRIVATE TRITON_MIN_COMPUTE_CAPABILITY=${TRITON_MIN_COMPUTE_CAPABILITY} ) target_link_libraries( simple PRIVATE - triton-common-async-work-queue # from repo-common - triton-common-error # from repo-common - triton-core-serverapi # from repo-core - triton-core-serverstub # from repo-core - ) + CUDA::cudart + ) +endif() # TRITON_ENABLE_GPU - if(${TRITON_ENABLE_GPU}) - target_compile_definitions( - simple - PRIVATE TRITON_ENABLE_GPU=1 - PRIVATE TRITON_MIN_COMPUTE_CAPABILITY=${TRITON_MIN_COMPUTE_CAPABILITY} - ) +install( + TARGETS simple + RUNTIME DESTINATION bin +) - target_link_libraries( - simple - PRIVATE - CUDA::cudart - ) - endif() # TRITON_ENABLE_GPU +# +# multi_server example +# +add_executable( + multi_server + multi_server.cc +) - install( - TARGETS simple - RUNTIME DESTINATION bin +target_compile_features(multi_server PRIVATE cxx_std_${TRITON_MIN_CXX_STANDARD}) +target_compile_options( + multi_server + PRIVATE + -Wall -Wextra -Wno-type-limits -Wno-unused-parameter -Wno-deprecated-declarations -Werror +) + +set_target_properties( + multi_server + PROPERTIES + POSITION_INDEPENDENT_CODE ON + SKIP_BUILD_RPATH TRUE + BUILD_WITH_INSTALL_RPATH TRUE + INSTALL_RPATH_USE_LINK_PATH FALSE + INSTALL_RPATH "" +) + +target_link_libraries( + multi_server + PRIVATE + triton-common-async-work-queue # from repo-common + triton-common-error # from repo-common + triton-core-serverapi # from repo-core + triton-core-serverstub # from repo-core + ) + +if(${TRITON_ENABLE_GPU}) + target_compile_definitions( + multi_server + PRIVATE TRITON_ENABLE_GPU=1 + PRIVATE TRITON_MIN_COMPUTE_CAPABILITY=${TRITON_MIN_COMPUTE_CAPABILITY} ) + target_link_libraries( + multi_server + PRIVATE + CUDA::cudart + ) +endif() # TRITON_ENABLE_GPU + +install( + TARGETS multi_server + RUNTIME DESTINATION bin +) + +if(${TRITON_ENABLE_GPU}) # - # multi_server example + # memory_alloc example # add_executable( - multi_server - multi_server.cc + memory_alloc + memory_alloc.cc ) - target_compile_features(multi_server PRIVATE cxx_std_${TRITON_MIN_CXX_STANDARD}) - if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") - message("Using MSVC as compiler, default target on Windows 10. " - "If the target system is not Windows 10, please update _WIN32_WINNT " - "to corresponding value.") - target_compile_options( - multi_server - PRIVATE - /W1 /D_WIN32_WINNT=0x0A00 /EHsc /Zc:preprocessor - ) - else() - target_compile_options( - multi_server - PRIVATE - -Wall -Wextra -Wno-type-limits -Wno-unused-parameter -Wno-deprecated-declarations -Werror - ) - endif() + target_compile_features(memory_alloc PRIVATE cxx_std_${TRITON_MIN_CXX_STANDARD}) + target_compile_options( + memory_alloc + PRIVATE + -Wall -Wextra -Wno-type-limits -Wno-unused-parameter -Wno-deprecated-declarations -Werror + ) set_target_properties( - multi_server + memory_alloc PROPERTIES POSITION_INDEPENDENT_CODE ON SKIP_BUILD_RPATH TRUE @@ -711,102 +673,30 @@ if (NOT WIN32) INSTALL_RPATH "" ) + target_compile_definitions( + memory_alloc + PRIVATE TRITON_ENABLE_GPU=1 + PRIVATE TRITON_MIN_COMPUTE_CAPABILITY=${TRITON_MIN_COMPUTE_CAPABILITY} + ) + target_link_libraries( - multi_server + memory_alloc PRIVATE triton-common-async-work-queue # from repo-common triton-common-error # from repo-common triton-core-serverapi # from repo-core triton-core-serverstub # from repo-core + CUDA::cudart ) - if(${TRITON_ENABLE_GPU}) - target_compile_definitions( - multi_server - PRIVATE TRITON_ENABLE_GPU=1 - PRIVATE TRITON_MIN_COMPUTE_CAPABILITY=${TRITON_MIN_COMPUTE_CAPABILITY} - ) - - target_link_libraries( - multi_server - PRIVATE - CUDA::cudart - ) - endif() # TRITON_ENABLE_GPU - install( - TARGETS multi_server + TARGETS memory_alloc RUNTIME DESTINATION bin ) +endif() # TRITON_ENABLE_GPU - if(${TRITON_ENABLE_GPU}) - # - # memory_alloc example - # - add_executable( - memory_alloc - memory_alloc.cc - ) - - target_compile_features(memory_alloc PRIVATE cxx_std_${TRITON_MIN_CXX_STANDARD}) - if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") - message("Using MSVC as compiler, default target on Windows 10. " - "If the target system is not Windows 10, please update _WIN32_WINNT " - "to corresponding value.") - target_compile_options( - memory_alloc - PRIVATE - /W1 /D_WIN32_WINNT=0x0A00 /EHsc /Zc:preprocessor - ) - else() - target_compile_options( - memory_alloc - PRIVATE - -Wall -Wextra -Wno-type-limits -Wno-unused-parameter -Wno-deprecated-declarations -Werror - ) - endif() - - set_target_properties( - memory_alloc - PROPERTIES - POSITION_INDEPENDENT_CODE ON - SKIP_BUILD_RPATH TRUE - BUILD_WITH_INSTALL_RPATH TRUE - INSTALL_RPATH_USE_LINK_PATH FALSE - INSTALL_RPATH "" - ) - - target_compile_definitions( - memory_alloc - PRIVATE TRITON_ENABLE_GPU=1 - PRIVATE TRITON_MIN_COMPUTE_CAPABILITY=${TRITON_MIN_COMPUTE_CAPABILITY} - ) - - target_link_libraries( - memory_alloc - PRIVATE - triton-common-async-work-queue # from repo-common - triton-common-error # from repo-common - triton-core-serverapi # from repo-core - triton-core-serverstub # from repo-core - CUDA::cudart - ) +# tritonfrontend python package +add_subdirectory(python) - install( - TARGETS memory_alloc - RUNTIME DESTINATION bin - ) - endif() # TRITON_ENABLE_GPU -endif() # NOT WIN32 - -# DLIS-7292: Extend tritonfrontend to build for Windows -if (NOT WIN32) - # tritonfrontend python package - add_subdirectory(python) -endif (NOT WIN32) - -# Currently unit tests do not build for windows... -if ( NOT WIN32) - add_subdirectory(test test) -endif() # NOT WIN32 +add_subdirectory(test test) diff --git a/src/grpc/CMakeLists.txt b/src/grpc/CMakeLists.txt index 0cd027a30a..3560c95e57 100644 --- a/src/grpc/CMakeLists.txt +++ b/src/grpc/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2023-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -38,19 +38,11 @@ add_library( ) target_compile_features(grpc-endpoint-library PRIVATE cxx_std_${TRITON_MIN_CXX_STANDARD}) -if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") - target_compile_options( - grpc-endpoint-library - PRIVATE - /W1 /D_WIN32_WINNT=0x0A00 /EHsc /Zc:preprocessor - ) -else() - target_compile_options( - grpc-endpoint-library - PRIVATE - -Wall -Wextra -Wno-unused-parameter -Wno-deprecated-declarations -Wno-error=maybe-uninitialized -Werror - ) -endif() +target_compile_options( + grpc-endpoint-library + PRIVATE + -Wall -Wextra -Wno-unused-parameter -Wno-deprecated-declarations -Wno-error=maybe-uninitialized -Werror +) set_target_properties( grpc-endpoint-library @@ -79,10 +71,7 @@ target_include_directories( PRIVATE $ ) -# FIXME when Triton support of OpenTelemetry is available on Windows -# add ${OPENTELEMETRY_CPP_INCLUDE_DIRS} to above target_include_directories -# JIRA DLIS-4786 -if (NOT WIN32 AND ${TRITON_ENABLE_TRACING}) +if (${TRITON_ENABLE_TRACING}) target_link_libraries( grpc-endpoint-library PRIVATE From c84bd8360ae95f6f9fffeff5322d81afd6be8ce0 Mon Sep 17 00:00:00 2001 From: Yingge He <157551214+yinggeh@users.noreply.github.com> Date: Thu, 4 Jun 2026 10:23:36 -0700 Subject: [PATCH 225/230] fix: Remove `all_special_tokens_extended` for transformers v5 compatibility in OpenAI frontend (#8817) --- python/openai/openai_frontend/engine/utils/tokenizer.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/python/openai/openai_frontend/engine/utils/tokenizer.py b/python/openai/openai_frontend/engine/utils/tokenizer.py index 8c21c63773..73184e843f 100644 --- a/python/openai/openai_frontend/engine/utils/tokenizer.py +++ b/python/openai/openai_frontend/engine/utils/tokenizer.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2024-2026, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -47,7 +47,6 @@ def get_cached_tokenizer( function caches these properties for faster access.""" tokenizer_all_special_ids = set(tokenizer.all_special_ids) - tokenizer_all_special_tokens_extended = tokenizer.all_special_tokens_extended tokenizer_all_special_tokens = set(tokenizer.all_special_tokens) tokenizer_len = len(tokenizer) @@ -60,10 +59,6 @@ def all_special_ids(self): def all_special_tokens(self): return tokenizer_all_special_tokens - @property - def all_special_tokens_extended(self): - return tokenizer_all_special_tokens_extended - def __len__(self): return tokenizer_len From 5dfefde83c2270cb293d37ad235ee95a6bb16856 Mon Sep 17 00:00:00 2001 From: mattwittwer Date: Thu, 4 Jun 2026 12:50:27 -0700 Subject: [PATCH 226/230] build: normalize install tree ownership and permissions (#8803) --- build.py | 15 ++++++++------- compose.py | 35 ++++++++++------------------------- 2 files changed, 18 insertions(+), 32 deletions(-) diff --git a/build.py b/build.py index 8f034d5234..9111c06c3a 100755 --- a/build.py +++ b/build.py @@ -1167,10 +1167,10 @@ def create_dockerfile_linux( df += f""" WORKDIR /opt -COPY --chown=1000:1000 build/install tritonserver +COPY build/install tritonserver WORKDIR /opt/tritonserver -COPY --chown=1000:1000 NVIDIA_Deep_Learning_Container_License.pdf . +COPY NVIDIA_Deep_Learning_Container_License.pdf . RUN find /opt/tritonserver/python -maxdepth 1 -type f -name \\ "tritonserver-*.whl" | xargs -I {{}} pip install --upgrade {{}}[{FLAGS.triton_wheels_dependencies_group}] && \\ find /opt/tritonserver/python -maxdepth 1 -type f -name \\ @@ -1185,7 +1185,7 @@ def create_dockerfile_linux( df += """ LABEL com.amazonaws.sagemaker.capabilities.accept-bind-to-port=true LABEL com.amazonaws.sagemaker.capabilities.multi-models=true -COPY --chown=1000:1000 docker/sagemaker/serve /usr/bin/. +COPY docker/sagemaker/serve /usr/bin/. """ # This is required since libcublasLt.so is not present during the build # stage of the PyTorch backend @@ -1248,8 +1248,9 @@ def dockerfile_prepare_container_linux(argmap, backends, enable_gpu, target_mach ENV TRITON_SERVER_GPU_ENABLED {gpu_enabled} # Create a user that can be used to run triton as -# non-root. Make sure that this user to given ID 1000. All server -# artifacts copied below are assign to this user. +# non-root. Make sure that this user is given ID 1000. Server +# artifacts copied below remain owned by root; the triton-server +# user reads and executes them via standard group/other permissions. ENV TRITON_SERVER_USER=triton-server RUN userdel tensorrt-server > /dev/null 2>&1 || true \\ && userdel ubuntu > /dev/null 2>&1 || true \\ @@ -2115,8 +2116,8 @@ def cibase_build( def finalize_build(cmake_script, install_dir, ci_dir): - cmake_script.cmd(f"chmod -R a+rw {install_dir}") - cmake_script.cmd(f"chmod -R a+rw {ci_dir}") + cmake_script.cmd(f"chmod -R u+rwX,go+rX,go-w {install_dir}") + cmake_script.cmd(f"chmod -R u+rwX,go+rX,go-w {ci_dir}") def enable_all(): diff --git a/compose.py b/compose.py index 4c26421ba3..ef1342c0fe 100755 --- a/compose.py +++ b/compose.py @@ -98,12 +98,12 @@ def start_dockerfile(ddir, images, argmap, dockerfile_name, backends): # Copy over files df += """ WORKDIR /opt/tritonserver -COPY --chown=1000:1000 --from=full /opt/tritonserver/LICENSE . -COPY --chown=1000:1000 --from=full /opt/tritonserver/TRITON_VERSION . -COPY --chown=1000:1000 --from=full /opt/tritonserver/NVIDIA_Deep_Learning_Container_License.pdf . -COPY --chown=1000:1000 --from=full /opt/tritonserver/bin bin/ -COPY --chown=1000:1000 --from=full /opt/tritonserver/lib lib/ -COPY --chown=1000:1000 --from=full /opt/tritonserver/include include/ +COPY --from=full /opt/tritonserver/LICENSE . +COPY --from=full /opt/tritonserver/TRITON_VERSION . +COPY --from=full /opt/tritonserver/NVIDIA_Deep_Learning_Container_License.pdf . +COPY --from=full /opt/tritonserver/bin bin/ +COPY --from=full /opt/tritonserver/lib lib/ +COPY --from=full /opt/tritonserver/include include/ """ with open(os.path.join(ddir, dockerfile_name), "w") as dfile: dfile.write(df) @@ -112,15 +112,10 @@ def start_dockerfile(ddir, images, argmap, dockerfile_name, backends): def add_requested_backends(ddir, dockerfile_name, backends): df = "# Copying over backends \n" for backend in backends: - df += """COPY --chown=1000:1000 --from=full /opt/tritonserver/backends/{} /opt/tritonserver/backends/{} + df += """COPY --from=full /opt/tritonserver/backends/{} /opt/tritonserver/backends/{} """.format( backend, backend ) - if len(backends) > 0: - df += """ -# Top-level /opt/tritonserver/backends not copied so need to explicitly set permissions here -RUN chown triton-server:triton-server /opt/tritonserver/backends -""" with open(os.path.join(ddir, dockerfile_name), "a") as dfile: dfile.write(df) @@ -128,15 +123,10 @@ def add_requested_backends(ddir, dockerfile_name, backends): def add_requested_repoagents(ddir, dockerfile_name, repoagents): df = "# Copying over repoagents \n" for ra in repoagents: - df += """COPY --chown=1000:1000 --from=full /opt/tritonserver/repoagents/{} /opt/tritonserver/repoagents/{} + df += """COPY --from=full /opt/tritonserver/repoagents/{} /opt/tritonserver/repoagents/{} """.format( ra, ra ) - if len(repoagents) > 0: - df += """ -# Top-level /opt/tritonserver/repoagents not copied so need to explicitly set permissions here -RUN chown triton-server:triton-server /opt/tritonserver/repoagents -""" with open(os.path.join(ddir, dockerfile_name), "a") as dfile: dfile.write(df) @@ -144,15 +134,10 @@ def add_requested_repoagents(ddir, dockerfile_name, repoagents): def add_requested_caches(ddir, dockerfile_name, caches): df = "# Copying over caches \n" for cache in caches: - df += """COPY --chown=1000:1000 --from=full /opt/tritonserver/caches/{} /opt/tritonserver/caches/{} + df += """COPY --from=full /opt/tritonserver/caches/{} /opt/tritonserver/caches/{} """.format( cache, cache ) - if len(caches) > 0: - df += """ -# Top-level /opt/tritonserver/caches not copied so need to explicitly set permissions here -RUN chown triton-server:triton-server /opt/tritonserver/caches -""" with open(os.path.join(ddir, dockerfile_name), "a") as dfile: dfile.write(df) @@ -163,7 +148,7 @@ def end_dockerfile(ddir, dockerfile_name, argmap): if argmap["SAGEMAKER_ENDPOINT"]: df += """ LABEL com.amazonaws.sagemaker.capabilities.accept-bind-to-port=true -COPY --chown=1000:1000 --from=full /usr/bin/serve /usr/bin/. +COPY --from=full /usr/bin/serve /usr/bin/. """ with open(os.path.join(ddir, dockerfile_name), "a") as dfile: dfile.write(df) From c3661e8cd8fcf69b0f560af65859810208308a13 Mon Sep 17 00:00:00 2001 From: "shantanu.mirajgave" Date: Fri, 5 Jun 2026 18:00:32 +0530 Subject: [PATCH 227/230] Initial commit --- CMakeLists.txt | 38 +- CONTRIBUTING.md | 2 +- Dockerfile.QA | 72 +- Dockerfile.sdk | 143 +- Dockerfile.win10.min | 201 ++ LICENSE | 2 +- README.md | 43 +- TRITON_VERSION | 2 +- build.py | 845 ++++++--- compose.py | 39 +- deploy/aws/values.yaml | 4 +- deploy/fleetcommand/Chart.yaml | 4 +- deploy/fleetcommand/values.yaml | 8 +- deploy/gcp/values.yaml | 4 +- .../perf-analyzer-script/triton_client.yaml | 4 +- .../server-deployer/build_and_push.sh | 8 +- .../server-deployer/chart/triton/Chart.yaml | 6 +- .../server-deployer/chart/triton/values.yaml | 8 +- .../server-deployer/data-test/schema.yaml | 8 +- .../server-deployer/schema.yaml | 8 +- .../gke-marketplace-app/trt-engine/README.md | 8 +- deploy/k8s-onprem/values.yaml | 4 +- deploy/mlflow-triton-plugin/README.md | 42 +- .../mlflow_triton/deployments.py | 19 +- deploy/oci/values.yaml | 4 +- docs/Dockerfile.docs | 12 +- docs/README.md | 18 +- docs/_static/rtd-data.js | 2 +- docs/client_guide/in_process.rst | 11 +- docs/client_guide/openai_readme.md | 345 +++- docs/client_guide/python.rst | 5 +- docs/client_guide/python_readme.rst | 268 +++ docs/conf.py | 98 +- docs/contents.rst | 15 +- docs/customization_guide/build.md | 140 +- docs/customization_guide/compose.md | 27 +- docs/customization_guide/deploy.md | 18 +- .../inference_protocols.md | 17 +- docs/customization_guide/test.md | 3 +- docs/examples/fetch_models.sh | 5 +- .../README.md | 4 +- .../inception_onnx/config.pbtxt | 8 +- docs/generate_docs.py | 369 ++-- docs/getting_started/llm.md | 18 +- ...nt.rst => quick_deployment_by_backend.rst} | 6 +- docs/getting_started/quick_start.rst | 175 ++ docs/getting_started/trtllm_user_guide.md | 8 +- docs/index.md | 12 +- docs/introduction/compatibility.md | 75 +- docs/introduction/index.md | 6 +- docs/introduction/release_notes.md | 8 +- docs/llm_features/speculative_decoding.rst | 36 +- .../speculative_decoding_by_backend_type.rst | 39 + docs/perf_benchmark/genai-perf-README.rst | 686 +++++++ docs/perf_benchmark/genai_perf.rst | 5 +- docs/perf_benchmark/model-analyzer-README.rst | 203 ++ docs/perf_benchmark/model_analyzer.rst | 28 +- docs/perf_benchmark/perf-analyzer-README.rst | 180 ++ docs/perf_benchmark/perf_analyzer.rst | 18 +- docs/protocol/extension_parameters.md | 11 +- docs/protocol/extension_schedule_policy.md | 6 +- docs/protocol/extension_shared_memory.md | 15 +- docs/repositories.txt | 1 + docs/user_guide/architecture.md | 771 +++++++- docs/user_guide/batcher.md | 10 +- docs/user_guide/custom_operations.md | 61 +- docs/user_guide/debugging_guide.md | 10 +- docs/user_guide/decoupled_models.md | 16 +- docs/user_guide/ensemble_models.md | 68 +- docs/user_guide/faq.md | 10 +- docs/user_guide/jetson.md | 12 +- docs/user_guide/metrics.md | 8 +- docs/user_guide/model_configuration.md | 648 ++++--- docs/user_guide/model_management.md | 365 ++-- docs/user_guide/model_repository.md | 364 ++-- docs/user_guide/optimization.md | 139 +- docs/user_guide/performance_tuning.md | 12 +- docs/user_guide/ragged_batching.md | 4 +- docs/user_guide/request_cancellation.md | 31 +- docs/user_guide/trace.md | 10 +- enhancements/NNNN-template-complete.md | 229 --- enhancements/NNNN-template-limited.md | 134 -- enhancements/README.md | 44 - enhancements/teps/0000-tep-process.md | 265 --- python/openai/README.md | 684 +------ .../openai/openai_frontend/engine/engine.py | 26 +- .../openai_frontend/engine/triton_engine.py | 939 ++------- .../openai_frontend/engine/utils/chat.py | 168 -- .../openai_frontend/engine/utils/tokenizer.py | 9 +- .../utils/tool_call_parsers/__init__.py | 40 - .../llama_tool_call_parser.py | 289 --- .../mistral_tool_call_parser.py | 318 ---- .../tool_call_parsers/tool_call_parser.py | 133 -- .../engine/utils/tool_call_parsers/utils.py | 129 -- .../openai_frontend/engine/utils/triton.py | 599 +----- .../frontend/fastapi/middleware/__init__.py | 25 - .../fastapi/middleware/api_restriction.py | 229 --- .../fastapi/middleware/request_size.py | 163 -- .../frontend/fastapi/routers/chat.py | 17 +- .../frontend/fastapi/routers/completions.py | 17 +- .../frontend/fastapi/routers/embeddings.py | 61 - .../fastapi/routers/model_management.py | 90 - .../frontend/fastapi/routers/models.py | 19 +- .../frontend/fastapi/routers/observability.py | 11 +- .../frontend/fastapi_frontend.py | 46 +- python/openai/openai_frontend/main.py | 121 +- .../openai/openai_frontend/schemas/openai.py | 141 +- python/openai/openai_frontend/utils/utils.py | 62 - python/openai/requirements.txt | 15 +- python/openai/tests/conftest.py | 162 +- python/openai/tests/test_chat_completions.py | 241 +-- python/openai/tests/test_completions.py | 166 +- python/openai/tests/test_embeddings.py | 600 ------ python/openai/tests/test_lora.py | 497 ----- python/openai/tests/test_model_management.py | 613 ------ .../tests/test_models/mock_llm/config.pbtxt | 8 +- python/openai/tests/test_openai_client.py | 502 +---- .../tests/test_openai_restricted_apis.py | 467 ----- python/openai/tests/test_request_size.py | 165 -- python/openai/tests/test_tool_calling.py | 594 ------ python/openai/tests/utils.py | 67 +- .../all-MiniLM-L6-v2/1/model.json | 1 - .../mistral-nemo-instruct-2407/1/model.json | 1 - .../mistral-nemo-instruct-2407/config.pbtxt | 28 - .../llama-3.1-8b-instruct/1/model.json | 2 +- qa/L0_backend_bls/test.sh | 5 +- qa/L0_backend_identity/identity_test.py | 51 +- qa/L0_backend_onnxruntime/test.py | 90 - qa/L0_backend_python/common.sh | 27 +- .../decoupled/decoupled_test.py | 195 +- .../decoupled_bls_async_cancel/1/model.py | 93 - .../decoupled_bls_async_cancel/config.pbtxt | 65 - .../models/decoupled_bls_cancel/1/model.py | 92 - .../models/decoupled_bls_cancel/config.pbtxt | 65 - .../1/model.py | 103 - .../config.pbtxt | 65 - qa/L0_backend_python/decoupled/test.sh | 6 +- qa/L0_backend_python/env/test.sh | 81 +- qa/L0_backend_python/env/zipslip_test.py | 88 - qa/L0_backend_python/examples/test.sh | 4 +- .../lifecycle/lifecycle_test.py | 66 +- qa/L0_backend_python/lifecycle/test.sh | 6 +- .../model_control/model_control_test.py | 241 +-- qa/L0_backend_python/model_control/test.sh | 15 +- qa/L0_backend_python/model_readiness/test.sh | 194 -- .../model_readiness/test_model_readiness.py | 518 ----- .../model.py | 63 - .../test_models/readiness_coroutine_model.py | 58 - .../test_models/readiness_model.py | 72 - qa/L0_backend_python/python_test.py | 13 +- .../setup_python_enviroment.sh | 10 +- qa/L0_backend_python/test.sh | 70 +- qa/L0_backend_python/variants/test.sh | 3 +- qa/L0_backend_tutorial/test.sh | 6 +- qa/L0_batch_custom/test.sh | 24 +- qa/L0_batcher/batcher_test.py | 111 +- qa/L0_batcher/test.sh | 68 +- qa/L0_buffer_attributes/test.sh | 4 +- qa/L0_client_build_variants/test.sh | 91 +- qa/L0_client_nobatch/test.sh | 2 +- qa/L0_client_timeout/test.sh | 4 +- .../cuda_shared_memory_test.py | 41 +- qa/L0_cuda_shared_memory/test.sh | 45 +- qa/L0_decoupled/decoupled_test.py | 240 +-- qa/L0_decoupled/test.sh | 2 +- qa/L0_dlpack_multi_gpu/test.sh | 18 +- qa/L0_grpc/python_grpc_aio_test.py | 8 +- qa/L0_grpc/python_unit_test.py | 91 +- qa/L0_grpc/test.sh | 53 +- qa/L0_http/generate_endpoint_test.py | 34 +- qa/L0_http/http_input_size_limit_test.py | 796 -------- qa/L0_http/http_request_many_chunks.py | 178 -- qa/L0_http/http_test.py | 264 +-- qa/L0_http/test.sh | 243 +-- qa/L0_http_fuzz/test.sh | 4 +- qa/L0_infer/infer_test.py | 694 +++---- qa/L0_infer/test.sh | 55 +- qa/L0_infer_reshape/infer_reshape_test.py | 13 +- qa/L0_infer_variable/infer_variable_test.py | 11 +- qa/L0_infer_variable/test.sh | 7 +- qa/L0_infer_zero/infer_zero_test.py | 6 +- .../input_validation_test.py | 87 +- qa/L0_input_validation/test.sh | 49 +- qa/L0_io/test.sh | 4 +- .../test.sh | 4 +- qa/L0_lifecycle/lifecycle_test.py | 39 +- qa/L0_lifecycle/test.sh | 14 +- qa/L0_logging/test.sh | 51 +- qa/L0_long_running_stress/scenarios.py | 12 +- qa/L0_long_running_stress/test.sh | 5 +- qa/L0_memory_growth/test.sh | 11 +- .../async_execute_decouple/1/model.py | 4 +- qa/L0_metrics/histogram_metrics_test.py | 42 +- qa/L0_metrics/metrics_config_test.py | 12 +- qa/L0_metrics/test.sh | 46 +- qa/L0_mlflow/plugin_test.py | 69 +- qa/L0_mlflow/test.sh | 12 +- .../expected | 1 - .../partial.pbtxt | 31 - .../allocation_strategy_no_key/partial.pbtxt | 26 - .../partial.pbtxt | 25 - .../allocation_strategy_value_1/expected | 1 - .../allocation_strategy_value_1/partial.pbtxt | 31 - .../allocation_strategy_value_2/expected | 1 - qa/L0_model_config/test.sh | 90 +- qa/L0_openai/generate_engine.py | 66 - qa/L0_openai/test.sh | 98 +- qa/L0_orca/test.sh | 94 +- qa/L0_parameters/class_count_test.py | 118 -- qa/L0_parameters/parameters_test.py | 421 ++--- qa/L0_parameters/test.sh | 83 +- qa/L0_passive_instance/test.sh | 5 +- .../nginx.conf} | 19 +- .../perf_analyzer_profile_export_schema.json | 95 + qa/L0_perf_analyzer/test.sh | 1164 ++++++++++++ qa/L0_perf_analyzer_capi/test.sh | 320 ++++ .../L0_perf_analyzer_doc_links/mkdocs.yml | 14 +- .../test.sh | 85 +- qa/L0_perf_analyzer_ground_truth/test.sh | 175 ++ qa/L0_perf_analyzer_report/test.sh | 175 ++ .../test.sh} | 47 +- qa/L0_perf_deeprecommender/run_test.sh | 4 +- qa/L0_perf_deeprecommender/test.sh | 21 +- qa/L0_perf_kaldi/test.sh | 3 +- qa/L0_perf_nomodel/run_test.sh | 9 +- qa/L0_perf_resnet/run_test.sh | 4 +- qa/L0_perf_resnet/test.sh | 60 +- qa/L0_perf_tensorrt_llm/test.sh | 131 +- qa/L0_perf_vllm/test.sh | 6 +- qa/L0_pinned_memory/test.sh | 4 +- qa/L0_query/test.sh | 4 +- .../grpc_cancellation_test.py | 57 +- qa/L0_request_cancellation/test.sh | 121 +- qa/L0_response_cache/generate_random_data.py | 59 - qa/L0_response_cache/response_cache_test | Bin 0 -> 5033616 bytes qa/L0_response_cache/test.sh | 101 - .../sagemaker_request_many_chunks.py | 173 -- qa/L0_sagemaker/test.sh | 259 +-- qa/L0_sdk/test.sh | 20 +- qa/L0_sequence_batcher/test.sh | 5 +- qa/L0_sequence_stress/test.sh | 41 +- qa/L0_shared_memory/shared_memory_test.py | 205 +- qa/L0_shared_memory/test.sh | 118 +- .../decoupled_producer/1/model.py | 57 - .../decoupled_producer/config.pbtxt | 57 - .../config.pbtxt | 75 - .../ensemble_backpressure_test.py | 554 ------ qa/L0_simple_ensemble/test.sh | 477 +---- qa/L0_storage_azure/test.sh | 118 +- qa/L0_storage_swiftstack/infer_test.py | 8 +- qa/L0_torch_aoti/test.sh | 148 -- qa/L0_torch_aoti/torch_aoti_infer_test.py | 284 --- qa/L0_trace/test.sh | 102 - qa/L0_trace/trace_stress_grpc_client.py | 90 - qa/L0_trt_bf16_dtype/trt_bf16_dtype_test.py | 10 +- qa/L0_trt_dynamic_shape/test.sh | 41 +- qa/L0_trt_reformat_free/test.sh | 4 +- qa/L0_trt_shape_tensors/test.sh | 15 +- qa/L0_vertex_ai/test.sh | 257 +-- qa/L0_warmup/test.sh | 4 +- qa/common/check_copyright.py | 1 - qa/common/check_valgrind_log.py | 4 +- qa/common/gen_common.py | 9 +- qa/common/gen_ensemble_model_utils.py | 20 +- qa/common/gen_jetson_trt_models | 9 +- qa/common/gen_qa_custom_ops | 149 ++ qa/common/gen_qa_custom_ops_models.py | 18 +- .../gen_qa_dyna_sequence_implicit_models.py | 47 +- qa/common/gen_qa_dyna_sequence_models.py | 92 +- qa/common/gen_qa_identity_models.py | 160 +- qa/common/gen_qa_image_models.py | 14 +- qa/common/gen_qa_implicit_models.py | 73 +- qa/common/gen_qa_model_repository | 909 ++++----- qa/common/gen_qa_models.py | 937 ++------- qa/common/gen_qa_ort_scalar_models.py | 6 +- qa/common/gen_qa_ragged_models.py | 58 +- qa/common/gen_qa_reshape_models.py | 74 +- qa/common/gen_qa_sequence_models.py | 92 +- qa/common/gen_qa_torchtrt_models.py | 18 +- qa/common/gen_qa_trt_data_dependent_shape.py | 18 +- qa/common/gen_qa_trt_format_models.py | 27 +- qa/common/gen_qa_trt_plugin_models.py | 48 +- qa/common/infer_test.py | 8 +- qa/common/infer_util.py | 39 +- qa/common/resnet50_labels.txt | 1000 ---------- qa/common/shm_util.py | 155 +- qa/common/test_util.py | 122 +- qa/common/trtllm_util.sh | 160 -- qa/common/util.sh | 9 +- qa/python_models/bls_memory/model.py | 4 +- qa/python_models/bls_memory_async/model.py | 4 +- .../decoupled_grpc_error/config.pbtxt | 60 - .../decoupled_grpc_error/model.py | 101 - qa/python_models/identity_bf16/model.py | 4 +- qa/python_models/join_add_sub/config.pbtxt | 60 - qa/python_models/join_add_sub/model.py | 72 - qa/python_models/python_version/model.py | 2 + .../config.pbtxt | 63 - .../response_sender_until_cancelled/model.py | 79 - .../torchvision/resnet50/model.py | 12 +- src/CMakeLists.txt | 434 +++-- src/classification.cc | 25 +- src/command_line_parser.cc | 53 +- src/command_line_parser.h | 9 +- src/common.cc | 70 +- src/common.h | 50 +- src/data_compressor.h | 110 +- src/grpc/CMakeLists.txt | 25 +- src/grpc/grpc_server.cc | 110 +- src/grpc/grpc_server.h | 19 - src/grpc/infer_handler.cc | 114 +- src/grpc/infer_handler.h | 99 +- src/grpc/stream_infer_handler.cc | 75 +- src/grpc/stream_infer_handler.h | 9 +- src/http_server.cc | 1676 +++++++++++------ src/http_server.h | 99 +- src/main.cc | 55 +- src/python/CMakeLists.txt | 4 +- src/python/build_wheel.py | 152 +- src/python/setup.py | 33 +- src/python/tritonfrontend/CMakeLists.txt | 2 +- src/python/tritonfrontend/_api/_kservegrpc.py | 1 - .../tritonfrontend/_api/_kservegrpc.pyi | 1 - src/sagemaker_server.cc | 194 +- src/sagemaker_server.h | 23 +- src/shared_memory_manager.cc | 62 +- src/shared_memory_manager.h | 22 +- src/simple.cc | 20 +- src/test/CMakeLists.txt | 53 +- src/test/data_compressor_test.cc | 214 +-- src/test/distributed_addsub/CMakeLists.txt | 4 +- src/test/dyna_sequence/CMakeLists.txt | 4 +- src/test/implicit_state/CMakeLists.txt | 4 +- src/test/iterative_sequence/CMakeLists.txt | 4 +- src/test/query_backend/CMakeLists.txt | 4 +- .../relocation_repoagent/CMakeLists.txt | 4 +- src/test/sequence/CMakeLists.txt | 4 +- src/test/tensor_size_test.cc | 491 ----- src/tracer.cc | 31 +- src/triton_signal.cc | 5 +- src/vertex_ai_server.cc | 38 +- src/vertex_ai_server.h | 6 +- tools/add_copyright.py | 20 +- 343 files changed, 12028 insertions(+), 26068 deletions(-) create mode 100644 Dockerfile.win10.min mode change 120000 => 100644 docs/client_guide/openai_readme.md create mode 100644 docs/client_guide/python_readme.rst rename docs/getting_started/{quick_deployment.rst => quick_deployment_by_backend.rst} (92%) create mode 100644 docs/getting_started/quick_start.rst create mode 100644 docs/llm_features/speculative_decoding_by_backend_type.rst create mode 100644 docs/perf_benchmark/genai-perf-README.rst create mode 100644 docs/perf_benchmark/model-analyzer-README.rst create mode 100644 docs/perf_benchmark/perf-analyzer-README.rst delete mode 100644 enhancements/NNNN-template-complete.md delete mode 100644 enhancements/NNNN-template-limited.md delete mode 100644 enhancements/README.md delete mode 100644 enhancements/teps/0000-tep-process.md delete mode 100644 python/openai/openai_frontend/engine/utils/chat.py delete mode 100644 python/openai/openai_frontend/engine/utils/tool_call_parsers/__init__.py delete mode 100644 python/openai/openai_frontend/engine/utils/tool_call_parsers/llama_tool_call_parser.py delete mode 100644 python/openai/openai_frontend/engine/utils/tool_call_parsers/mistral_tool_call_parser.py delete mode 100644 python/openai/openai_frontend/engine/utils/tool_call_parsers/tool_call_parser.py delete mode 100644 python/openai/openai_frontend/engine/utils/tool_call_parsers/utils.py delete mode 100644 python/openai/openai_frontend/frontend/fastapi/middleware/__init__.py delete mode 100644 python/openai/openai_frontend/frontend/fastapi/middleware/api_restriction.py delete mode 100644 python/openai/openai_frontend/frontend/fastapi/middleware/request_size.py delete mode 100644 python/openai/openai_frontend/frontend/fastapi/routers/embeddings.py delete mode 100644 python/openai/openai_frontend/frontend/fastapi/routers/model_management.py delete mode 100644 python/openai/openai_frontend/utils/utils.py delete mode 100644 python/openai/tests/test_embeddings.py delete mode 100644 python/openai/tests/test_lora.py delete mode 100644 python/openai/tests/test_model_management.py delete mode 100755 python/openai/tests/test_openai_restricted_apis.py delete mode 100644 python/openai/tests/test_request_size.py delete mode 100644 python/openai/tests/test_tool_calling.py delete mode 100644 python/openai/tests/vllm_embedding_models/all-MiniLM-L6-v2/1/model.json delete mode 100644 python/openai/tests/vllm_mistral_models/mistral-nemo-instruct-2407/1/model.json delete mode 100644 python/openai/tests/vllm_mistral_models/mistral-nemo-instruct-2407/config.pbtxt delete mode 100755 qa/L0_backend_onnxruntime/test.py delete mode 100644 qa/L0_backend_python/decoupled/models/decoupled_bls_async_cancel/1/model.py delete mode 100644 qa/L0_backend_python/decoupled/models/decoupled_bls_async_cancel/config.pbtxt delete mode 100644 qa/L0_backend_python/decoupled/models/decoupled_bls_cancel/1/model.py delete mode 100644 qa/L0_backend_python/decoupled/models/decoupled_bls_cancel/config.pbtxt delete mode 100644 qa/L0_backend_python/decoupled/models/decoupled_bls_cancel_after_complete/1/model.py delete mode 100644 qa/L0_backend_python/decoupled/models/decoupled_bls_cancel_after_complete/config.pbtxt delete mode 100755 qa/L0_backend_python/env/zipslip_test.py delete mode 100755 qa/L0_backend_python/model_readiness/test.sh delete mode 100644 qa/L0_backend_python/model_readiness/test_model_readiness.py delete mode 100644 qa/L0_backend_python/model_readiness/test_models/is_ready_fn_returns_true_decoupled/model.py delete mode 100644 qa/L0_backend_python/model_readiness/test_models/readiness_coroutine_model.py delete mode 100644 qa/L0_backend_python/model_readiness/test_models/readiness_model.py delete mode 100755 qa/L0_http/http_input_size_limit_test.py delete mode 100755 qa/L0_http/http_request_many_chunks.py delete mode 100644 qa/L0_model_config/custom_parameters/tensorrt/invalid/allocation_strategy_invalid_value/expected delete mode 100644 qa/L0_model_config/custom_parameters/tensorrt/invalid/allocation_strategy_invalid_value/partial.pbtxt delete mode 100644 qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_no_key/partial.pbtxt delete mode 100644 qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_no_parameters/partial.pbtxt delete mode 100644 qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_1/expected delete mode 100644 qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_1/partial.pbtxt delete mode 100644 qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_2/expected delete mode 100644 qa/L0_openai/generate_engine.py delete mode 100755 qa/L0_parameters/class_count_test.py rename qa/{L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_2/partial.pbtxt => L0_perf_analyzer/nginx.conf} (81%) create mode 100644 qa/L0_perf_analyzer/perf_analyzer_profile_export_schema.json create mode 100755 qa/L0_perf_analyzer/test.sh create mode 100755 qa/L0_perf_analyzer_capi/test.sh rename python/openai/tests/vllm_embedding_models/all-MiniLM-L6-v2/config.pbtxt => qa/L0_perf_analyzer_doc_links/mkdocs.yml (85%) rename qa/{L0_backend_onnxruntime => L0_perf_analyzer_doc_links}/test.sh (55%) create mode 100755 qa/L0_perf_analyzer_ground_truth/test.sh create mode 100755 qa/L0_perf_analyzer_report/test.sh rename qa/{L0_backend_python/model_readiness/test_models/is_ready_fn_returns_true_decoupled/config.pbtxt => L0_perf_analyzer_unit_tests/test.sh} (75%) mode change 100644 => 100755 delete mode 100755 qa/L0_response_cache/generate_random_data.py create mode 100755 qa/L0_response_cache/response_cache_test delete mode 100755 qa/L0_sagemaker/sagemaker_request_many_chunks.py delete mode 100644 qa/L0_simple_ensemble/backpressure_test_models/decoupled_producer/1/model.py delete mode 100644 qa/L0_simple_ensemble/backpressure_test_models/decoupled_producer/config.pbtxt delete mode 100644 qa/L0_simple_ensemble/backpressure_test_models/ensemble_disabled_max_inflight_requests/config.pbtxt delete mode 100755 qa/L0_simple_ensemble/ensemble_backpressure_test.py delete mode 100755 qa/L0_torch_aoti/test.sh delete mode 100755 qa/L0_torch_aoti/torch_aoti_infer_test.py delete mode 100755 qa/L0_trace/trace_stress_grpc_client.py create mode 100755 qa/common/gen_qa_custom_ops delete mode 100644 qa/common/resnet50_labels.txt delete mode 100755 qa/common/trtllm_util.sh delete mode 100644 qa/python_models/decoupled_grpc_error/config.pbtxt delete mode 100644 qa/python_models/decoupled_grpc_error/model.py delete mode 100644 qa/python_models/join_add_sub/config.pbtxt delete mode 100644 qa/python_models/join_add_sub/model.py delete mode 100644 qa/python_models/response_sender_until_cancelled/config.pbtxt delete mode 100644 qa/python_models/response_sender_until_cancelled/model.py delete mode 100644 src/test/tensor_size_test.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index bd91a52109..211e7578ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2020-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2020-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -24,7 +24,22 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -cmake_minimum_required(VERSION 3.31.8) +cmake_minimum_required(VERSION 3.18) + +# CMake 4.0+ rejects projects that call cmake_minimum_required(VERSION < 3.5). +# The fetched third_party repo builds libevent 2.1.12 via ExternalProject using a +# separate CMake invocation; that step does not inherit -D variables from the top +# level unless they are also in the environment. If configure fails inside libevent +# with "Compatibility with CMake < 3.5 has been removed", use either: +# export CMAKE_POLICY_VERSION_MINIMUM=3.5 # before cmake AND cmake --build +# or install/use CMake 3.28.x–3.31.x for this build. +if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.0") + message( + STATUS + "CMake ${CMAKE_VERSION}: for libevent/third_party with CMake 4.x, export " + "CMAKE_POLICY_VERSION_MINIMUM=3.5 in your shell before configure and build " + "(see docs/customization_guide/build.md).") +endif() project(tritonserver LANGUAGES C CXX) @@ -166,9 +181,22 @@ if (OPENSSL_ROOT_DIR) set(_CMAKE_ARGS_OPENSSL_ROOT_DIR "-DOPENSSL_ROOT_DIR:PATH=${OPENSSL_ROOT_DIR}") endif() -set(_FINDPACKAGE_PROTOBUF_CONFIG_DIR "${TRITON_THIRD_PARTY_INSTALL_PREFIX}/protobuf/${LIB_DIR}/cmake/protobuf") +# Location where protobuf-config.cmake will be installed varies by +# platform +if (WIN32) + set(_FINDPACKAGE_PROTOBUF_CONFIG_DIR "${TRITON_THIRD_PARTY_INSTALL_PREFIX}/protobuf/cmake") +else() + set(_FINDPACKAGE_PROTOBUF_CONFIG_DIR "${TRITON_THIRD_PARTY_INSTALL_PREFIX}/protobuf/${LIB_DIR}/cmake/protobuf") +endif() -set(_FINDPACKAGE_OPENTELEMETRY_CONFIG_DIR "${TRITON_THIRD_PARTY_INSTALL_PREFIX}/opentelemetry-cpp/${LIB_DIR}/cmake/opentelemetry-cpp") +# Triton with Opentelemetry is not supported on Windows +# FIXME: add location for Windows, when support is added +# JIRA DLIS-4786 +if (WIN32) + set(_FINDPACKAGE_OPENTELEMETRY_CONFIG_DIR "") +else() + set(_FINDPACKAGE_OPENTELEMETRY_CONFIG_DIR "${TRITON_THIRD_PARTY_INSTALL_PREFIX}/opentelemetry-cpp/${LIB_DIR}/cmake/opentelemetry-cpp") +endif() if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(TRITON_INSTALL_PREFIX ${CMAKE_CURRENT_BINARY_DIR}/install) @@ -189,7 +217,7 @@ endif() # TRITON_ENABLE_HTTP || TRITON_ENABLE_METRICS || TRITON_ENABLE_SAGEMAKER if(${TRITON_ENABLE_GRPC}) set(TRITON_DEPENDS ${TRITON_DEPENDS} grpc) endif() # TRITON_ENABLE_GRPC -if(${TRITON_ENABLE_TRACING}) +if(NOT WIN32 AND ${TRITON_ENABLE_TRACING}) set(TRITON_DEPENDS ${TRITON_DEPENDS} opentelemetry-cpp) endif() # TRITON_ENABLE_TRACING diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0d01b1a996..6b9d3378dc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,7 +88,7 @@ proposed change so that the Triton team can provide feedback. documentation for instructions on running these tests. - Triton Inference Server's default build assumes recent versions of - dependencies (CUDA, PyTorch, TensorRT, + dependencies (CUDA, TensorFlow, PyTorch, TensorRT, etc.). Contributions that add compatibility with older versions of those dependencies will be considered, but NVIDIA cannot guarantee that all possible build configurations work, are not broken by diff --git a/Dockerfile.QA b/Dockerfile.QA index caa5fb0e2c..7406cf2345 100644 --- a/Dockerfile.QA +++ b/Dockerfile.QA @@ -1,4 +1,4 @@ -# Copyright 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -59,23 +59,35 @@ RUN apt-get update && \ libboost-dev \ python3-dev \ python3-pip \ + python3-wheel \ python3-setuptools \ python3-venv \ - python3-wheel \ rapidjson-dev \ software-properties-common && \ rm -rf /var/lib/apt/lists/* -RUN pip3 install cmake==4.0.3 -ENV CMAKE_POLICY_VERSION_MINIMUM=3.5 +RUN apt update -q=2 \ + && apt install -y gpg wget \ + && wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null \ + && . /etc/os-release \ + && echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $UBUNTU_CODENAME main" | tee /etc/apt/sources.list.d/kitware.list >/dev/null \ + && apt-get update -q=2 \ + && apt-get install -y --no-install-recommends cmake=3.28.3* cmake-data=3.28.3* -# Add densenet_onnx model to example repo +# Add inception_graphdef model to example repo # FIXME: This should be changed to using the fetch_models.sh script # in order to ensure the public facing docs are up-to-date. WORKDIR /workspace/docs/examples/model_repository -RUN mkdir -p densenet_onnx/1 && \ - wget -O densenet_onnx/1/model.onnx \ - https://github.com/onnx/models/raw/main/validated/vision/classification/densenet-121/model/densenet-7.onnx +RUN mkdir -p model_repository/inception_onnx/1 && \ + wget -O /tmp/inception_v3_2016_08_28_frozen.pb.tar.gz \ + https://storage.googleapis.com/download.tensorflow.org/models/inception_v3_2016_08_28_frozen.pb.tar.gz && \ + (cd /tmp && tar xzf inception_v3_2016_08_28_frozen.pb.tar.gz) && \ + python3 -m venv tf2onnx && \ + source ./tf2onnx/bin/activate && \ + pip3 install "numpy<2" tensorflow tf2onnx && \ + python3 -m tf2onnx.convert --graphdef /tmp/inception_v3_2016_08_28_frozen.pb --output inception_v3_onnx.model.onnx --inputs input:0 --outputs InceptionV3/Predictions/Softmax:0 && \ + deactivate && \ + mv inception_v3_onnx.model.onnx model_repository/inception_onnx/1/model.onnx # Update the qa/ directory with test executables, models, etc. WORKDIR /workspace @@ -105,7 +117,7 @@ RUN mkdir -p qa/common && \ cp -r docs/examples/model_repository/simple_identity qa/L0_grpc/models && \ cp -r docs/examples/model_repository/simple_sequence qa/L0_grpc/models && \ cp -r docs/examples/model_repository/simple_string qa/L0_grpc/models && \ - cp -r docs/examples/model_repository/densenet_onnx qa/L0_grpc/models && \ + cp -r docs/examples/model_repository/inception_onnx qa/L0_grpc/models && \ mkdir qa/L0_grpc_state_cleanup/models && \ cp -r /workspace/src/test/models/repeat_int32 qa/L0_grpc_state_cleanup/models/ && \ mkdir qa/L0_http/models && \ @@ -114,7 +126,7 @@ RUN mkdir -p qa/common && \ cp -r docs/examples/model_repository/simple_identity qa/L0_http/models && \ cp -r docs/examples/model_repository/simple_sequence qa/L0_http/models && \ cp -r docs/examples/model_repository/simple_string qa/L0_http/models && \ - cp -r docs/examples/model_repository/densenet_onnx qa/L0_http/models && \ + cp -r docs/examples/model_repository/inception_onnx qa/L0_grpc/models && \ mkdir qa/L0_https/models && \ cp -r docs/examples/model_repository/simple qa/L0_https/models/. && \ mkdir qa/L0_secure_grpc/models && \ @@ -139,7 +151,6 @@ RUN mkdir -p qa/common && \ mkdir qa/L0_data_compression/models && \ cp -r docs/examples/model_repository/simple qa/L0_data_compression/models && \ cp bin/data_compressor_test qa/L0_data_compression/. && \ - cp bin/tensor_size_test qa/L0_input_validation/. && \ cp bin/metrics_api_test qa/L0_metrics/. && \ cp bin/response_cache_test qa/L0_response_cache/. && \ cp bin/request_cancellation_test qa/L0_request_cancellation/. && \ @@ -174,8 +185,8 @@ RUN mkdir -p qa/custom_models/custom_sequence_int32/1 && \ qa/custom_models/custom_dyna_sequence_int32/1/. # L0_lifecycle needs No-GPU build of identity backend. -WORKDIR /workspace/tritonbuild/identity -RUN rm -rf install build && mkdir build && cd build && \ +RUN cd tritonbuild/identity && \ + rm -rf install build && mkdir build && cd build && \ cmake -DTRITON_ENABLE_GPU=OFF \ -DCMAKE_INSTALL_PREFIX:PATH=/workspace/tritonbuild/identity/install \ -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ @@ -186,18 +197,15 @@ RUN rm -rf install build && mkdir build && cd build && \ make -j16 install # L0_backend_python test require triton_shm_monitor -ARG TRITON_BOOST_URL="https://archives.boost.io/release/1.80.0/source/boost_1_80_0.tar.gz" -WORKDIR /workspace/tritonbuild/python -RUN rm -rf install build && mkdir build && cd build && \ +RUN cd tritonbuild/python && \ + rm -rf install build && mkdir build && cd build && \ cmake -DCMAKE_INSTALL_PREFIX:PATH=/workspace/tritonbuild/python/install \ -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ -DTRITON_COMMON_REPO_TAG:STRING=${TRITON_COMMON_REPO_TAG} \ -DTRITON_CORE_REPO_TAG:STRING=${TRITON_CORE_REPO_TAG} \ - -DTRITON_BOOST_URL:STRING=${TRITON_BOOST_URL} \ -DTRITON_BACKEND_REPO_TAG:STRING=${TRITON_BACKEND_REPO_TAG} .. && \ make -j16 triton-shm-monitor install -WORKDIR /workspace/ RUN cp tritonbuild/identity/install/backends/identity/libtriton_identity.so \ qa/L0_lifecycle/. && \ cp tritonbuild/python/install/backends/python/triton_shm_monitor*.so \ @@ -317,9 +325,7 @@ ENV DEBIAN_FRONTEND=noninteractive RUN if grep -qE '^VERSION_ID="(18\.04|20\.04|22\.04|24\.04)' /etc/os-release; then \ apt-get update && \ apt-get install -y --no-install-recommends \ - libpng-dev && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*; \ + libpng-dev; \ else \ echo "Ubuntu version must be either 18.04, 20.04, 22.04 or 24.04" && \ exit 1; \ @@ -330,23 +336,22 @@ RUN if grep -qE '^VERSION_ID="(18\.04|20\.04|22\.04|24\.04)' /etc/os-release; th RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ gdb \ + libopencv-dev \ libarchive-dev \ libopencv-core-dev \ - libopencv-dev \ libzmq3-dev \ + openjdk-11-jdk \ nginx \ npm \ - openjdk-11-jdk \ protobuf-compiler \ python3-dev \ python3-pip \ python3-protobuf \ - python3-setuptools \ python3-wheel \ + python3-setuptools \ swig \ valgrind && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* \ + rm -rf /var/lib/apt/lists/* # CI/QA expects "python" executable (not python3). RUN rm -f /usr/bin/python && \ @@ -378,17 +383,10 @@ COPY --chown=1000:1000 --from=sdk /workspace/qa/ qa/ # Remove CI tests that are meant to run only on build image and # install the tritonserver/triton python client APIs. -RUN rm -fr qa/L0_copyrights qa/L0_build_variants - - -RUN --mount=type=secret,id=triton_ci_pip_extra_values,env=TRITON_CI_PYPI_EXTRA_VALUES \ - if [ -n "${TRITON_CI_PYPI_EXTRA_VALUES}" ]; then \ - find qa/pkgs/ -maxdepth 1 -type f -name \ - "tritonclient-*any*.whl" -exec pip3 install --upgrade ${TRITON_CI_PYPI_EXTRA_VALUES} {}[all] \; ; \ - else \ - find qa/pkgs/ -maxdepth 1 -type f -name \ - "tritonclient-*any*.whl" -exec pip3 install --upgrade {}[all] \; ; \ - fi +RUN rm -fr qa/L0_copyrights qa/L0_build_variants && \ + find qa/pkgs/ -maxdepth 1 -type f -name \ + "tritonclient-*linux*.whl" | xargs printf -- '%s[all]' | \ + xargs pip3 install --upgrade ENV LD_LIBRARY_PATH /opt/tritonserver/qa/clients:${LD_LIBRARY_PATH} diff --git a/Dockerfile.sdk b/Dockerfile.sdk index b2181abe6e..de6b12df5e 100644 --- a/Dockerfile.sdk +++ b/Dockerfile.sdk @@ -1,4 +1,4 @@ -# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -29,19 +29,22 @@ # # Base image on the minimum Triton container -ARG BASE_IMAGE=nvcr.io/nvidia/tritonserver:26.05-py3-min +ARG BASE_IMAGE=nvcr.io/nvidia/tritonserver:25.03-py3-min ARG TRITON_CLIENT_REPO_SUBDIR=clientrepo +ARG TRITON_PA_REPO_SUBDIR=perfanalyzerrepo ARG TRITON_REPO_ORGANIZATION=http://github.com/triton-inference-server ARG TRITON_COMMON_REPO_TAG=main ARG TRITON_CORE_REPO_TAG=main ARG TRITON_CLIENT_REPO_TAG=main ARG TRITON_THIRD_PARTY_REPO_TAG=main +ARG TRITON_MODEL_ANALYZER_REPO_TAG=main ARG TRITON_ENABLE_GPU=ON ARG JAVA_BINDINGS_MAVEN_VERSION=3.8.4 ARG JAVA_BINDINGS_JAVACPP_PRESETS_TAG=1.5.8 + # DCGM version to install for Model Analyzer -ARG DCGM_VERSION=4.5.3-1 +ARG DCGM_VERSION=3.3.6 ARG NVIDIA_TRITON_SERVER_SDK_VERSION=unknown ARG NVIDIA_BUILD_ID=unknown @@ -54,39 +57,48 @@ FROM ${BASE_IMAGE} AS sdk_build # Ensure apt-get won't prompt for selecting options ENV DEBIAN_FRONTEND=noninteractive -ENV PIP_BREAK_SYSTEM_PACKAGES=1 CMAKE_POLICY_VERSION_MINIMUM=3.5 +ENV PIP_BREAK_SYSTEM_PACKAGES=1 RUN apt-get update && \ apt-get install -y --no-install-recommends \ + ca-certificates \ + software-properties-common \ autoconf \ automake \ build-essential \ - ca-certificates \ curl \ git \ gperf \ libb64-dev \ libgoogle-perftools-dev \ - libopencv-core-dev \ libopencv-dev \ + libopencv-core-dev \ libssl-dev \ libtool \ - maven \ - openjdk-11-jdk \ pkg-config \ python3 \ - python3-dev \ - python3-pdfkit \ python3-pip \ - python3-setuptools \ + python3-dev \ python3-wheel \ + python3-setuptools \ rapidjson-dev \ - software-properties-common \ vim \ - wget && \ - pip3 install --upgrade "grpcio-tools<1.68" cmake==4.0.3 auditwheel - -ENV CMAKE_POLICY_MINIMUM_REQUIRED=3.5 + wget \ + python3-pdfkit \ + openjdk-11-jdk \ + maven && \ + pip3 install --upgrade "grpcio-tools<1.68" + +# Client build requires recent version of CMake (FetchContent required) +# Using CMAKE installation instruction from:: https://apt.kitware.com/ +RUN apt update -q=2 \ + && apt install -y gpg wget \ + && wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null \ + && . /etc/os-release \ + && echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $UBUNTU_CODENAME main" | tee /etc/apt/sources.list.d/kitware.list >/dev/null \ + && apt-get update -q=2 \ + && apt-get install -y --no-install-recommends cmake=3.28.3* cmake-data=3.28.3* \ + && cmake --version # Build expects "python" executable (not python3). RUN rm -f /usr/bin/python && \ @@ -95,6 +107,7 @@ RUN rm -f /usr/bin/python && \ # Build the client library and examples ARG TRITON_REPO_ORGANIZATION ARG TRITON_CLIENT_REPO_SUBDIR +ARG TRITON_PA_REPO_SUBDIR ARG TRITON_COMMON_REPO_TAG ARG TRITON_CORE_REPO_TAG ARG TRITON_CLIENT_REPO_TAG @@ -107,6 +120,7 @@ ARG TARGETPLATFORM WORKDIR /workspace COPY TRITON_VERSION . COPY ${TRITON_CLIENT_REPO_SUBDIR} client +COPY ${TRITON_PA_REPO_SUBDIR} perf_analyzer WORKDIR /workspace/client_build RUN cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ @@ -117,11 +131,42 @@ RUN cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ -DTRITON_THIRD_PARTY_REPO_TAG=${TRITON_THIRD_PARTY_REPO_TAG} \ -DTRITON_ENABLE_PERF_ANALYZER=OFF \ -DTRITON_ENABLE_CC_HTTP=ON -DTRITON_ENABLE_CC_GRPC=ON \ - -DTRITON_ENABLE_PYTHON_HTTP=ON -DTRITON_ENABLE_PYTHON_GRPC=ON \ + -DTRITON_ENABLE_PYTHON_HTTP=OFF -DTRITON_ENABLE_PYTHON_GRPC=OFF \ -DTRITON_ENABLE_JAVA_HTTP=ON \ -DTRITON_ENABLE_EXAMPLES=ON -DTRITON_ENABLE_TESTS=ON \ -DTRITON_ENABLE_GPU=${TRITON_ENABLE_GPU} /workspace/client -RUN cmake --build . -v --parallel --target cc-clients java-clients python-clients +RUN make -j16 cc-clients java-clients && \ + rm -fr ~/.m2 + +# TODO: PA will rebuild the CC clients since it depends on it. +# This should be optimized so that we do not have to build +# the CC clients twice. Similarly, because the SDK expectation is +# that PA is packaged with the python client, we hold off on building +# the python client until now. Post-migration we should focus +# effort on de-tangling these flows. +WORKDIR /workspace/pa_build +RUN cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ + -DTRITON_VERSION=`cat /workspace/TRITON_VERSION` \ + -DTRITON_REPO_ORGANIZATION=${TRITON_REPO_ORGANIZATION} \ + -DTRITON_COMMON_REPO_TAG=${TRITON_COMMON_REPO_TAG} \ + -DTRITON_CORE_REPO_TAG=${TRITON_CORE_REPO_TAG} \ + -DTRITON_CLIENT_REPO_TAG=${TRITON_CLIENT_REPO_TAG} \ + -DTRITON_ENABLE_PERF_ANALYZER_C_API=ON \ + -DTRITON_ENABLE_PERF_ANALYZER_TFS=ON \ + -DTRITON_ENABLE_PERF_ANALYZER_TS=ON \ + -DTRITON_ENABLE_PERF_ANALYZER_OPENAI=ON \ + -DTRITON_ENABLE_CC_HTTP=ON \ + -DTRITON_ENABLE_CC_GRPC=ON \ + -DTRITON_ENABLE_PYTHON_HTTP=ON \ + -DTRITON_ENABLE_PYTHON_GRPC=ON \ + -DTRITON_PACKAGE_PERF_ANALYZER=ON \ + -DTRITON_ENABLE_GPU=${TRITON_ENABLE_GPU} \ + /workspace/perf_analyzer +RUN make -j16 perf-analyzer python-clients + +RUN pip3 install build \ + && cd /workspace/perf_analyzer/genai-perf \ + && python3 -m build --wheel --outdir /workspace/install/python # Install Java API Bindings RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ @@ -149,40 +194,37 @@ ARG TRITON_ENABLE_GPU RUN apt-get update && \ apt-get install -y --no-install-recommends \ + software-properties-common \ curl \ - default-jdk \ git \ gperf \ libb64-dev \ libgoogle-perftools-dev \ - libopencv-core-dev \ libopencv-dev \ + libopencv-core-dev \ libssl-dev \ libtool \ - maven \ - perl \ python3 \ - python3-dev \ - python3-pdfkit \ python3-pip \ - python3-setuptools \ + python3-dev \ python3-wheel \ + python3-setuptools \ vim \ - wget && \ - pip3 install "grpcio<1.68" "grpcio-tools<1.68" && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*; + wget \ + python3-pdfkit \ + maven \ + default-jdk && \ + pip3 install "grpcio<1.68" "grpcio-tools<1.68" WORKDIR /workspace COPY TRITON_VERSION . COPY NVIDIA_Deep_Learning_Container_License.pdf . COPY --from=sdk_build /workspace/client/ client/ +COPY --from=sdk_build /workspace/perf_analyzer/ perf_analyzer/ COPY --from=sdk_build /workspace/install/ install/ -WORKDIR /workspace/install -RUN export VERSION=`cat /workspace/TRITON_VERSION` && \ - tar zcf /workspace/v"$VERSION".clients.tar.gz ./* - -WORKDIR /workspace +RUN cd install && \ + export VERSION=`cat /workspace/TRITON_VERSION` && \ + tar zcf /workspace/v$VERSION.clients.tar.gz * # For CI testing need to copy over L0_sdk test and L0_client_build_variants test. RUN mkdir qa @@ -196,38 +238,23 @@ COPY --from=sdk_build /workspace/client/src/python/library/tests/* qa/python_cli # Install an image needed by the quickstart and other documentation. COPY qa/images/mug.jpg images/mug.jpg +RUN pip3 install install/python/genai_perf-*.whl + # Install the dependencies needed to run the client examples. These # are not needed for building but including them allows this image to # be used to run the client examples. -RUN pip3 install --upgrade "numpy<2" pillow attrdict - -RUN --mount=type=secret,id=triton_ci_pip_extra_values,env=TRITON_CI_PYPI_EXTRA_VALUES \ - if [ -n "${TRITON_CI_PYPI_EXTRA_VALUES}" ]; then \ - find install/python/ -maxdepth 1 -type f -name \ - "tritonclient-*any*.whl" -exec pip3 install --upgrade ${TRITON_CI_PYPI_EXTRA_VALUES} {}[all] \; ; \ - else \ - find install/python/ -maxdepth 1 -type f -name \ - "tritonclient-*any*.whl" -exec pip3 install --upgrade {}[all] \; ; \ - fi - -# Install GenAI-Perf -RUN --mount=type=secret,id=triton_ci_pip_extra_values,env=TRITON_CI_PYPI_EXTRA_VALUES \ - if [ -n "${TRITON_CI_PYPI_EXTRA_VALUES}" ]; then \ - pip3 install --upgrade ${TRITON_CI_PYPI_EXTRA_VALUES} genai-perf ; \ - else \ - pip3 install --upgrade genai-perf ; \ - fi +RUN pip3 install --upgrade "numpy<2" pillow attrdict && \ + find install/python/ -maxdepth 1 -type f -name \ + "tritonclient-*linux*.whl" | xargs printf -- '%s[all]' | \ + xargs pip3 install --upgrade # Install DCGM RUN if [ "$TRITON_ENABLE_GPU" = "ON" ]; then \ [ "$(uname -m)" != "x86_64" ] && arch="sbsa" || arch="x86_64" && \ curl -o /tmp/cuda-keyring.deb \ - "https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/${arch}/cuda-keyring_1.1-1_all.deb" \ - && apt -y install /tmp/cuda-keyring.deb && rm /tmp/cuda-keyring.deb && \ - apt update && \ - apt install --yes --no-install-recommends \ - datacenter-gpu-manager-4-core=1:${DCGM_VERSION} \ - datacenter-gpu-manager-4-dev=1:${DCGM_VERSION}; \ + https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/$arch/cuda-keyring_1.1-1_all.deb \ + && apt install /tmp/cuda-keyring.deb && rm /tmp/cuda-keyring.deb && \ + apt-get update && apt-get install -y datacenter-gpu-manager=1:${DCGM_VERSION}; \ fi # Build expects "python" executable (not python3). diff --git a/Dockerfile.win10.min b/Dockerfile.win10.min new file mode 100644 index 0000000000..b883ad4dcb --- /dev/null +++ b/Dockerfile.win10.min @@ -0,0 +1,201 @@ +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# Windows min container for Triton build + +ARG BASE_IMAGE=mcr.microsoft.com/windows:10.0.19042.1889 + +FROM ${BASE_IMAGE} as dependency_base + +RUN powershell.exe Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope LocalMachine +RUN powershell.exe [Net.ServicePointManager]::Expect100Continue=$true;[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls,[Net.SecurityProtocolType]::Tls11,[Net.SecurityProtocolType]::Tls12,[Net.SecurityProtocolType]::Ssl3;Invoke-Expression( New-Object System.Net.WebClient ).DownloadString('https://chocolatey.org/install.ps1') +RUN choco install unzip -y + +# +# Installing TensorRT +# +ARG TENSORRT_VERSION=10.8.0.43 +ARG TENSORRT_ZIP="TensorRT-${TENSORRT_VERSION}.Windows.win10.cuda-12.8.zip" +ARG TENSORRT_SOURCE=https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.8.0/zip/TensorRT-${TENSORRT_VERSION}.Windows.win10.cuda-12.8.zip +# COPY ${TENSORRT_ZIP} /tmp/${TENSORRT_ZIP} +ADD ${TENSORRT_SOURCE} /tmp/${TENSORRT_ZIP} +RUN unzip /tmp/%TENSORRT_ZIP% +RUN move TensorRT-* TensorRT + +LABEL TENSORRT_VERSION="${TENSORRT_VERSION}" + + +# +# Installing cuDNN +# +ARG CUDNN_VERSION=9.7.1.26 +ARG CUDNN_ZIP=cudnn-windows-x86_64-${CUDNN_VERSION}_cuda12-archive.zip +ARG CUDNN_SOURCE=https://developer.download.nvidia.com/compute/cudnn/redist/cudnn/windows-x86_64/cudnn-windows-x86_64-9.7.1.26_cuda12-archive.zip +ADD ${CUDNN_SOURCE} /tmp/${CUDNN_ZIP} +RUN unzip /tmp/%CUDNN_ZIP% +RUN move cudnn-* cudnn + +LABEL CUDNN_VERSION="${CUDNN_VERSION}" + + +FROM ${BASE_IMAGE} as build_base + +SHELL ["cmd", "/S", "/C"] + +RUN mkdir c:\tmp +WORKDIR /tmp + +RUN powershell.exe Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope LocalMachine +RUN powershell.exe [Net.ServicePointManager]::Expect100Continue=$true;[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls,[Net.SecurityProtocolType]::Tls11,[Net.SecurityProtocolType]::Tls12,[Net.SecurityProtocolType]::Ssl3;Invoke-Expression( New-Object System.Net.WebClient ).DownloadString('https://chocolatey.org/install.ps1') +RUN choco install git docker unzip -y + +# +# Installing python +# +ARG PYTHON_VERSION=3.12.3 +ARG PYTHON_SOURCE=https://www.python.org/ftp/python/${PYTHON_VERSION}/python-${PYTHON_VERSION}-amd64.exe +ADD ${PYTHON_SOURCE} python-${PYTHON_VERSION}-amd64.exe +RUN python-%PYTHON_VERSION%-amd64.exe /quiet InstallAllUsers=1 PrependPath=1 Include_doc=0 TargetDir="C:\python%PYTHON_VERSION%" +RUN mklink "C:\python%PYTHON_VERSION%\python3.exe" "C:\python%PYTHON_VERSION%\python.exe" +RUN pip install --upgrade wheel setuptools docker + +LABEL PYTHON_VERSION=${PYTHON_VERSION} + +# +# Installing CMake +# +ARG CMAKE_VERSION=3.30.5 +RUN pip install cmake==%CMAKE_VERSION% + +ENV CMAKE_TOOLCHAIN_FILE /vcpkg/scripts/buildsystems/vcpkg.cmake +ENV VCPKG_TARGET_TRIPLET x64-windows + +LABEL CMAKE_VERSION=${CMAKE_VERSION} + +# Be aware that pip can interact badly with VS cmd shell so need to pip install before +# vsdevcmd.bat (see https://bugs.python.org/issue38989) +# +# Installing Visual Studio BuildTools: VS17 2022 +# +# Download collect.exe in case of an install failure. +ADD https://aka.ms/vscollect.exe "C:\tmp\collect.exe" + +# Use the latest release channel. For more control, specify the location of an internal layout. +# Download the Build Tools bootstrapper. +# ARG BUILD_TOOLS_SOURCE=https://aka.ms/vs/17/release/vs_buildtools.exe + +ARG BUILDTOOLS_VERSION=17.12.35506.116 +ARG BUILD_TOOLS_SOURCE=https://download.visualstudio.microsoft.com/download/pr/5536698c-711c-4834-876f-2817d31a2ef2/58894fc272e86d3c3a6d85bf3a1df1e5a0685be8b9ab65d9f3cc5c2a8c6921cc/vs_BuildTools.exe + +ADD ${BUILD_TOOLS_SOURCE} vs_buildtools.exe +# Install Build Tools with the Microsoft.VisualStudio.Workload.VCTools workload, including recommended. +ARG VS_INSTALL_PATH_WP="C:\BuildTools" +RUN vs_buildtools.exe --quiet --wait --norestart --nocache install \ + --installPath %VS_INSTALL_PATH_WP% \ + --add Microsoft.VisualStudio.Workload.VCTools \ + --includeRecommended \ + --locale "En-us" + +LABEL BUILDTOOLS_VERSION=${BUILDTOOLS_VERSION} + +WORKDIR / + +# +# Installing Vcpkg +# +ARG VCPGK_VERSION=2024.03.19 +RUN git clone --single-branch --depth=1 -b %VCPGK_VERSION% https://github.com/microsoft/vcpkg.git +WORKDIR /vcpkg +RUN bootstrap-vcpkg.bat +RUN vcpkg.exe update +RUN vcpkg.exe install \ + boost-interprocess:x64-windows \ + boost-stacktrace:x64-windows \ + b64:x64-windows \ + openssl-windows:x64-windows \ + openssl:x64-windows \ + pthread:x64-windows \ + rapidjson:x64-windows \ + zlib:x64-windows +RUN vcpkg.exe integrate install + +LABEL VCPGK_VERSION=${VCPGK_VERSION} + +WORKDIR / + +# +# Installing CUDA +# +ARG CUDA_MAJOR=12 +ARG CUDA_MINOR=8 +ARG CUDA_PATCH=0 +ARG CUDA_VERSION=${CUDA_MAJOR}.${CUDA_MINOR}.${CUDA_PATCH} +ARG CUDA_PACKAGES="nvcc_${CUDA_MAJOR}.${CUDA_MINOR} \ + cudart_${CUDA_MAJOR}.${CUDA_MINOR} \ + nvml_dev_${CUDA_MAJOR}.${CUDA_MINOR} \ + nvrtc_${CUDA_MAJOR}.${CUDA_MINOR} nvrtc_dev_${CUDA_MAJOR}.${CUDA_MINOR} \ + cublas_${CUDA_MAJOR}.${CUDA_MINOR} cublas_dev_${CUDA_MAJOR}.${CUDA_MINOR} \ + cufft_${CUDA_MAJOR}.${CUDA_MINOR} cufft_dev_${CUDA_MAJOR}.${CUDA_MINOR} \ + curand_${CUDA_MAJOR}.${CUDA_MINOR} curand_dev_${CUDA_MAJOR}.${CUDA_MINOR} \ + cusolver_${CUDA_MAJOR}.${CUDA_MINOR} cusolver_dev_${CUDA_MAJOR}.${CUDA_MINOR} \ + cusparse_${CUDA_MAJOR}.${CUDA_MINOR} cusparse_dev_${CUDA_MAJOR}.${CUDA_MINOR} \ + cupti_${CUDA_MAJOR}.${CUDA_MINOR} \ + thrust_${CUDA_MAJOR}.${CUDA_MINOR} \ + visual_studio_integration_${CUDA_MAJOR}.${CUDA_MINOR}" +ARG CUDA_INSTALL_ROOT_WP="C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v${CUDA_MAJOR}.${CUDA_MINOR}" + +ARG CUDA_SOURCE=https://developer.download.nvidia.com/compute/cuda/${CUDA_VERSION}/network_installers/cuda_${CUDA_VERSION}_windows_network.exe +ADD ${CUDA_SOURCE} cuda_${CUDA_VERSION}_windows_network.exe + +RUN cuda_%CUDA_VERSION%_windows_network.exe -s %CUDA_PACKAGES% +# Copy the CUDA visualstudio integration from where it was installed +# into the appropriate place in BuildTools +RUN copy "%CUDA_INSTALL_ROOT_WP%\extras\visual_studio_integration\MSBuildExtensions\*" "%VS_INSTALL_PATH_WP%\MSBuild\Microsoft\VC\v170\BuildCustomizations" + +RUN setx PATH "%CUDA_INSTALL_ROOT_WP%\bin;%PATH%" + +ENV CUDA_VERSION=${CUDA_VERSION} +LABEL CUDA_VERSION="${CUDA_VERSION}" + +ARG CUDNN_VERSION=9.7.1.26 +ENV CUDNN_VERSION ${CUDNN_VERSION} +COPY --from=dependency_base /cudnn /cudnn +RUN copy cudnn\bin\cudnn*.dll "%CUDA_INSTALL_ROOT_WP%\bin\." +RUN copy cudnn\lib\x64\cudnn*.lib "%CUDA_INSTALL_ROOT_WP%\lib\x64\." +RUN copy cudnn\include\cudnn*.h "%CUDA_INSTALL_ROOT_WP%\include\." +LABEL CUDNN_VERSION="${CUDNN_VERSION}" + +ARG TENSORRT_VERSION=10.8.0.43 +ENV TRT_VERSION ${TENSORRT_VERSION} +COPY --from=dependency_base /TensorRT /TensorRT +RUN setx PATH "c:\TensorRT\lib;%PATH%" +LABEL TENSORRT_VERSION="${TENSORRT_VERSION}" + +# It is important that the entrypoint initialize VisualStudio +# environment otherwise the build will fail. Also set +# CMAKE_TOOLCHAIN_FILE and VCPKG_TARGET_TRIPLET so +# that cmake can find the packages installed by vcpkg. +ENTRYPOINT C:\BuildTools\VC\Auxiliary\Build\vcvars64.bat && diff --git a/LICENSE b/LICENSE index 6c65d3b34d..d367cc74ad 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2018-2026, NVIDIA CORPORATION. All rights reserved. +Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions diff --git a/README.md b/README.md index fdb6b2a5bf..65d4918be5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![License](https://img.shields.io/badge/License-BSD3-lightgrey.svg)](https://opensource.org/licenses/BSD-3-Clause) - ->[!WARNING] ->You are currently on the `main` branch which tracks under-development progress ->towards the next release. The current release is version [2.69.0](https://github.com/triton-inference-server/server/releases/latest) ->and corresponds to the 26.05 container release on NVIDIA GPU Cloud (NGC). # Triton Inference Server Triton Inference Server is an open source inference serving software that streamlines AI inferencing. Triton enables teams to deploy any AI model from multiple deep learning and machine learning frameworks, including TensorRT, -PyTorch, ONNX, OpenVINO, Python, RAPIDS FIL, and more. Triton +TensorFlow, PyTorch, ONNX, OpenVINO, Python, RAPIDS FIL, and more. Triton Inference Server supports inference across cloud, data center, edge and embedded devices on NVIDIA GPUs, x86 and ARM CPU, or AWS Inferentia. Triton Inference Server delivers optimized performance for many query types, including real time, @@ -54,14 +48,14 @@ Major features include: frameworks](https://github.com/triton-inference-server/fil_backend) - [Concurrent model execution](docs/user_guide/architecture.md#concurrent-model-execution) -- [Dynamic batching](docs/user_guide/batcher.md#dynamic-batcher) -- [Sequence batching](docs/user_guide/batcher.md#sequence-batcher) and +- [Dynamic batching](docs/user_guide/model_configuration.md#dynamic-batcher) +- [Sequence batching](docs/user_guide/model_configuration.md#sequence-batcher) and [implicit state management](docs/user_guide/architecture.md#implicit-state-management) for stateful models - Provides [Backend API](https://github.com/triton-inference-server/backend) that allows adding custom backends and pre/post processing operations - Supports writing custom backends in python, a.k.a. - [Python-based backends.](https://github.com/triton-inference-server/backend/blob/main/docs/python_based_backends.md#python-based-backends) + [Python-based backends.](https://github.com/triton-inference-server/backend/blob/r25.03/docs/python_based_backends.md#python-based-backends) - Model pipelines using [Ensembling](docs/user_guide/architecture.md#ensemble-models) or [Business Logic Scripting @@ -70,8 +64,8 @@ Major features include: protocols](docs/customization_guide/inference_protocols.md) based on the community developed [KServe protocol](https://github.com/kserve/kserve/tree/master/docs/predict-api/v2) -- A [C API](docs/customization_guide/inprocess_c_api.md) and - [Java API](docs/customization_guide/inprocess_java_api.md) +- A [C API](docs/customization_guide/inference_protocols.md#in-process-triton-server-api) and + [Java API](docs/customization_guide/inference_protocols.md#java-bindings-for-in-process-triton-server-api) allow Triton to link directly into your application for edge and other in-process use cases - [Metrics](docs/user_guide/metrics.md) indicating GPU utilization, server throughput, server latency, and more @@ -90,16 +84,16 @@ Inference Server with the ```bash # Step 1: Create the example model repository -git clone -b r26.05 https://github.com/triton-inference-server/server.git +git clone -b r25.03 https://github.com/triton-inference-server/server.git cd server/docs/examples ./fetch_models.sh # Step 2: Launch triton from the NGC Triton container -docker run --gpus=1 --rm --net=host -v ${PWD}/model_repository:/models nvcr.io/nvidia/tritonserver:26.05-py3 tritonserver --model-repository=/models --model-control-mode explicit --load-model densenet_onnx +docker run --gpus=1 --rm --net=host -v ${PWD}/model_repository:/models nvcr.io/nvidia/tritonserver:25.03-py3 tritonserver --model-repository=/models --model-control-mode explicit --load-model densenet_onnx # Step 3: Sending an Inference Request # In a separate console, launch the image_client example from the NGC Triton SDK container -docker run -it --rm --net=host nvcr.io/nvidia/tritonserver:26.05-py3-sdk /workspace/install/bin/image_client -m densenet_onnx -c 3 -s INCEPTION /workspace/images/mug.jpg +docker run -it --rm --net=host nvcr.io/nvidia/tritonserver:25.03-py3-sdk /workspace/install/bin/image_client -m densenet_onnx -c 3 -s INCEPTION /workspace/images/mug.jpg # Inference should return the following Image '/workspace/images/mug.jpg': @@ -134,6 +128,7 @@ images. - [Install Triton Inference Server without Docker containers](docs/customization_guide/build.md#building-without-docker) - [Build a custom Triton Inference Server Docker container](docs/customization_guide/compose.md) - [Build Triton Inference Server from source](docs/customization_guide/build.md#building-on-unsupported-platforms) +- [Build Triton Inference Server for Windows 10](docs/customization_guide/build.md#building-for-windows-10) - Examples for deploying Triton Inference Server with Kubernetes and Helm on [GCP](deploy/gcp/README.md), [AWS](deploy/aws/README.md), and [NVIDIA FleetCommand](deploy/fleetcommand/README.md) - [Secure Deployment Considerations](docs/customization_guide/deploy.md) @@ -165,16 +160,17 @@ configuration](docs/user_guide/model_configuration.md) for the model. - Triton supports multiple execution engines, called [backends](https://github.com/triton-inference-server/backend#where-can-i-find-all-the-backends-that-are-available-for-triton), including [TensorRT](https://github.com/triton-inference-server/tensorrt_backend), + [TensorFlow](https://github.com/triton-inference-server/tensorflow_backend), [PyTorch](https://github.com/triton-inference-server/pytorch_backend), [ONNX](https://github.com/triton-inference-server/onnxruntime_backend), [OpenVINO](https://github.com/triton-inference-server/openvino_backend), [Python](https://github.com/triton-inference-server/python_backend), and more - Not all the above backends are supported on every platform supported by Triton. Look at the - [Backend-Platform Support Matrix](https://github.com/triton-inference-server/backend/blob/main/docs/backend_platform_support_matrix.md) + [Backend-Platform Support Matrix](https://github.com/triton-inference-server/backend/blob/r25.03/docs/backend_platform_support_matrix.md) to learn which backends are supported on your target platform. - Learn how to [optimize performance](docs/user_guide/optimization.md) using the - [Performance Analyzer](https://github.com/triton-inference-server/perf_analyzer/blob/main/README.md) + [Performance Analyzer](https://github.com/triton-inference-server/perf_analyzer/blob/r25.03/README.md) and [Model Analyzer](https://github.com/triton-inference-server/model_analyzer) - Learn how to [manage loading and unloading models](docs/user_guide/model_management.md) in @@ -188,14 +184,14 @@ A Triton *client* application sends inference and other requests to Triton. The [Python and C++ client libraries](https://github.com/triton-inference-server/client) provide APIs to simplify this communication. -- Review client examples for [C++](https://github.com/triton-inference-server/client/blob/main/src/c%2B%2B/examples), - [Python](https://github.com/triton-inference-server/client/blob/main/src/python/examples), - and [Java](https://github.com/triton-inference-server/client/blob/main/src/java/src/main/java/triton/client/examples) +- Review client examples for [C++](https://github.com/triton-inference-server/client/blob/r25.03/src/c%2B%2B/examples), + [Python](https://github.com/triton-inference-server/client/blob/r25.03/src/python/examples), + and [Java](https://github.com/triton-inference-server/client/blob/r25.03/src/java/src/main/java/triton/client/examples) - Configure [HTTP](https://github.com/triton-inference-server/client#http-options) and [gRPC](https://github.com/triton-inference-server/client#grpc-options) client options - Send input data (e.g. a jpeg image) directly to Triton in the [body of an HTTP - request without any additional metadata](https://github.com/triton-inference-server/server/blob/main/docs/protocol/extension_binary_data.md#raw-binary-request) + request without any additional metadata](https://github.com/triton-inference-server/server/blob/r25.03/docs/protocol/extension_binary_data.md#raw-binary-request) ### Extend Triton @@ -204,7 +200,7 @@ designed for modularity and flexibility - [Customize Triton Inference Server container](docs/customization_guide/compose.md) for your use case - [Create custom backends](https://github.com/triton-inference-server/backend) - in either [C/C++](https://github.com/triton-inference-server/backend/blob/main/README.md#triton-backend-api) + in either [C/C++](https://github.com/triton-inference-server/backend/blob/r25.03/README.md#triton-backend-api) or [Python](https://github.com/triton-inference-server/python_backend) - Create [decoupled backends and models](docs/user_guide/decoupled_models.md) that can send multiple responses for a request or not send any responses for a request @@ -258,3 +254,4 @@ For questions, we recommend posting in our community Please refer to the [NVIDIA Developer Triton page](https://developer.nvidia.com/nvidia-triton-inference-server) for more information. + diff --git a/TRITON_VERSION b/TRITON_VERSION index 6a166a54c5..5f46e11eed 100644 --- a/TRITON_VERSION +++ b/TRITON_VERSION @@ -1 +1 @@ -2.70.0dev +2.56.0 diff --git a/build.py b/build.py index 9111c06c3a..5a0f96413a 100755 --- a/build.py +++ b/build.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2020-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -71,13 +71,14 @@ # DEFAULT_TRITON_VERSION_MAP = { - "release_version": "2.70.0dev", - "triton_container_version": "26.06dev", - "upstream_container_version": "26.05", - "ort_version": "1.24.4", - "ort_openvino_version": "2026.1.0", - "standalone_openvino_version": "2026.1.0", - "dcgm_version": "4.5.3-1", + "release_version": "2.56.0", + "triton_container_version": "25.03", + "upstream_container_version": "25.03", + "ort_version": "1.21.0", + "ort_openvino_version": "2025.0.0", + "standalone_openvino_version": "2025.0.0", + "dcgm_version": "3.3.6", + "vllm_version": "0.7.3", "rhel_py_version": "3.12.3", } @@ -118,8 +119,6 @@ def fail_if(p, msg): def target_platform(): # When called by compose.py, FLAGS will be None if FLAGS and FLAGS.target_platform is not None: - if FLAGS.target_platform == "windows": - fail("Windows is no longer a supported target platform.") return FLAGS.target_platform platform_string = platform.system().lower() if platform_string == "linux": @@ -130,8 +129,6 @@ def target_platform(): return "linux" else: return "rhel" - elif platform_string == "windows": - fail("Windows is no longer a supported target platform.") else: return platform_string @@ -171,6 +168,13 @@ def __del__(self): def close(self): if self._file is not None: + if target_platform() == "windows": + self.blankln() + self._file.write("}\n") + self._file.write("catch {\n") + self._file.write(" $_;\n") + self._file.write(" ExitWithCode 1;\n") + self._file.write("}\n") """Close the file""" self._file.close() self._file = None @@ -198,7 +202,8 @@ def comment_verbose(self, msg=""): self.comment(msg) def header(self, desc=None): - self._file.write("#!/usr/bin/env bash\n\n") + if target_platform() != "windows": + self._file.write("#!/usr/bin/env bash\n\n") if desc is not None: self.comment() @@ -207,12 +212,27 @@ def header(self, desc=None): self.blankln() self.comment("Exit script immediately if any command fails") - self._file.write("set -e\n") - if self._verbose: - self._file.write("set -x\n") + if target_platform() == "windows": + self._file.write("$UseStructuredOutput = $false\n") + self.blankln() + self._file.write("function ExitWithCode($exitcode) {\n") + self._file.write(" $host.SetShouldExit($exitcode)\n") + self._file.write(" exit $exitcode\n") + self._file.write("}\n") + self.blankln() + if self._verbose: + self._file.write("Set-PSDebug -Trace 1\n") + self.blankln() + self._file.write("try {\n") + else: + self._file.write("set -e\n") + if self._verbose: + self._file.write("set -x\n") self.blankln() def envvar_ref(self, v): + if target_platform() == "windows": + return f"${{env:{v}}}" return f"${{{v}}}" def cmd(self, clist, check_exitcode=False): @@ -223,23 +243,54 @@ def cmd(self, clist, check_exitcode=False): self._file.write(f"{c} ") self.blankln() + if check_exitcode: + if target_platform() == "windows": + self._file.write("if ($LASTEXITCODE -ne 0) {\n") + self._file.write( + ' Write-Output "exited with status code $LASTEXITCODE";\n' + ) + self._file.write(" ExitWithCode 1;\n") + self._file.write("}\n") + def cwd(self, path): - self.cmd(f"cd {path}") + if target_platform() == "windows": + self.cmd(f"Set-Location -EV Err -EA Stop {path}") + else: + self.cmd(f"cd {path}") def cp(self, src, dest): - self.cmd(f"cp {src} {dest}") + if target_platform() == "windows": + self.cmd(f"Copy-Item -EV Err -EA Stop {src} -Destination {dest}") + else: + self.cmd(f"cp {src} {dest}") def mkdir(self, path): - self.cmd(f"mkdir -p {pathlib.Path(path)}") + if target_platform() == "windows": + self.cmd( + f"New-Item -EV Err -EA Stop -ItemType Directory -Force -Path {path}" + ) + else: + self.cmd(f"mkdir -p {pathlib.Path(path)}") def rmdir(self, path): - self.cmd(f"rm -fr {pathlib.Path(path)}") + if target_platform() == "windows": + self.cmd(f"if (Test-Path -Path {path}) {{") + self.cmd(f" Remove-Item -EV Err -EA Stop -Recurse -Force {path}") + self.cmd("}") + else: + self.cmd(f"rm -fr {pathlib.Path(path)}") def cpdir(self, src, dest): - self.cmd(f"cp -r {src} {dest}") + if target_platform() == "windows": + self.cmd(f"Copy-Item -EV Err -EA Stop -Recurse {src} -Destination {dest}") + else: + self.cmd(f"cp -r {src} {dest}") def tar(self, subdir, tar_filename): - self.cmd(f"tar zcf {tar_filename} {subdir}") + if target_platform() == "windows": + fail("unsupported operation: tar") + else: + self.cmd(f"tar zcf {tar_filename} {subdir}") def cmake(self, args): # Pass some additional envvars into cmake... @@ -259,7 +310,10 @@ def gitclone(self, repo, tag, subdir, org): if not FLAGS.no_force_clone: self.rmdir(clone_dir) - self.cmd(f"if [[ ! -e {clone_dir} ]]; then") + if target_platform() == "windows": + self.cmd(f"if (-Not (Test-Path -Path {clone_dir})) {{") + else: + self.cmd(f"if [[ ! -e {clone_dir} ]]; then") # FIXME [DLIS-4045 - Currently the tag starting with "pull/" is not # working with "--repo-tag" as the option is not forwarded to the @@ -269,19 +323,19 @@ def gitclone(self, repo, tag, subdir, org): # reference onto a new branch we name "tritonbuildref". if tag.startswith("pull/"): self.cmd( - f" git clone --recursive --depth=1 {org}/{repo}.git {subdir}; git --git-dir {subdir}/.git log --oneline -1", + f" git clone --recursive --depth=1 {org}/{repo}.git {subdir};", check_exitcode=True, ) - self.cmd("fi") + self.cmd("}" if target_platform() == "windows" else "fi") self.cwd(subdir) self.cmd(f"git fetch origin {tag}:tritonbuildref", check_exitcode=True) self.cmd(f"git checkout tritonbuildref", check_exitcode=True) else: self.cmd( - f" git clone --recursive --single-branch --depth=1 -b {tag} {org}/{repo}.git {subdir}; git --git-dir {subdir}/.git log --oneline -1", + f" git clone --recursive --single-branch --depth=1 -b {tag} {org}/{repo}.git {subdir};", check_exitcode=True, ) - self.cmd("fi") + self.cmd("}" if target_platform() == "windows" else "fi") def cmake_core_arg(name, type, value): @@ -551,7 +605,14 @@ def backend_cmake_args(images, components, be, install_dir, library_paths): cmake_backend_enable(be, "TRITON_ENABLE_METRICS", FLAGS.enable_metrics) ) - if target_platform() == "igpu": + # [DLIS-4950] always enable below once Windows image is updated with CUPTI + # cargs.append(cmake_backend_enable(be, 'TRITON_ENABLE_MEMORY_TRACKER', True)) + if (target_platform() == "windows") and (not FLAGS.no_container_build): + print( + "Warning: Detected docker build is used for Windows, backend utility 'device memory tracker' will be disabled due to missing library in CUDA Windows docker image." + ) + cargs.append(cmake_backend_enable(be, "TRITON_ENABLE_MEMORY_TRACKER", False)) + elif target_platform() == "igpu": print( "Warning: Detected iGPU build, backend utility 'device memory tracker' will be disabled as iGPU doesn't contain required version of the library." ) @@ -561,7 +622,7 @@ def backend_cmake_args(images, components, be, install_dir, library_paths): cargs += cmake_backend_extra_args(be) if be == "tensorrtllm": - cargs.append("-S ../triton_backend/inflight_batcher_llm -B .") + cargs.append("-S ../inflight_batcher_llm -B .") else: cargs.append("..") @@ -599,10 +660,6 @@ def pytorch_cmake_args(images): cargs.append( cmake_backend_enable("pytorch", "TRITON_ENABLE_NVTX", FLAGS.enable_nvtx) ) - if target_platform() == "igpu": - cargs.append( - cmake_backend_enable("pytorch", "TRITON_PYTORCH_NVSHMEM", False) - ) return cargs @@ -630,51 +687,59 @@ def onnxruntime_cmake_args(images, library_paths): ) ) - if "base" in images: - cargs.append( - cmake_backend_arg( - "onnxruntime", "TRITON_BUILD_CONTAINER", None, images["base"] + if target_platform() == "windows": + if "base" in images: + cargs.append( + cmake_backend_arg( + "onnxruntime", "TRITON_BUILD_CONTAINER", None, images["base"] + ) ) - ) else: - cargs.append( - cmake_backend_arg( - "onnxruntime", - "TRITON_BUILD_CONTAINER_VERSION", - None, - FLAGS.upstream_container_version, + if "base" in images: + cargs.append( + cmake_backend_arg( + "onnxruntime", "TRITON_BUILD_CONTAINER", None, images["base"] + ) + ) + else: + cargs.append( + cmake_backend_arg( + "onnxruntime", + "TRITON_BUILD_CONTAINER_VERSION", + None, + FLAGS.triton_container_version, + ) ) - ) - # TODO: TPRD-333 OpenVino extension is not currently supported by our manylinux build - if ( - (target_machine() != "aarch64") - and (target_platform() != "rhel") - and (FLAGS.ort_openvino_version is not None) - ): - cargs.append( - cmake_backend_enable( - "onnxruntime", "TRITON_ENABLE_ONNXRUNTIME_OPENVINO", True + # TODO: TPRD-333 OpenVino extension is not currently supported by our manylinux build + if ( + (target_machine() != "aarch64") + and (target_platform() != "rhel") + and (FLAGS.ort_openvino_version is not None) + ): + cargs.append( + cmake_backend_enable( + "onnxruntime", "TRITON_ENABLE_ONNXRUNTIME_OPENVINO", True + ) ) - ) - cargs.append( - cmake_backend_arg( - "onnxruntime", - "TRITON_BUILD_ONNXRUNTIME_OPENVINO_VERSION", - None, - FLAGS.ort_openvino_version, + cargs.append( + cmake_backend_arg( + "onnxruntime", + "TRITON_BUILD_ONNXRUNTIME_OPENVINO_VERSION", + None, + FLAGS.ort_openvino_version, + ) ) - ) - if (target_platform() == "igpu") or (target_platform() == "rhel"): - cargs.append( - cmake_backend_arg( - "onnxruntime", - "TRITON_BUILD_TARGET_PLATFORM", - None, - target_platform(), + if (target_platform() == "igpu") or (target_platform() == "rhel"): + cargs.append( + cmake_backend_arg( + "onnxruntime", + "TRITON_BUILD_TARGET_PLATFORM", + None, + target_platform(), + ) ) - ) return cargs @@ -688,21 +753,29 @@ def openvino_cmake_args(): FLAGS.standalone_openvino_version, ) ] - if "base" in images: - cargs.append( - cmake_backend_arg( - "openvino", "TRITON_BUILD_CONTAINER", None, images["base"] + if target_platform() == "windows": + if "base" in images: + cargs.append( + cmake_backend_arg( + "openvino", "TRITON_BUILD_CONTAINER", None, images["base"] + ) ) - ) else: - cargs.append( - cmake_backend_arg( - "openvino", - "TRITON_BUILD_CONTAINER_VERSION", - None, - FLAGS.upstream_container_version, + if "base" in images: + cargs.append( + cmake_backend_arg( + "openvino", "TRITON_BUILD_CONTAINER", None, images["base"] + ) + ) + else: + cargs.append( + cmake_backend_arg( + "openvino", + "TRITON_BUILD_CONTAINER_VERSION", + None, + FLAGS.upstream_container_version, + ) ) - ) return cargs @@ -710,6 +783,12 @@ def tensorrt_cmake_args(): cargs = [ cmake_backend_enable("tensorrt", "TRITON_ENABLE_NVTX", FLAGS.enable_nvtx), ] + if target_platform() == "windows": + cargs.append( + cmake_backend_arg( + "tensorrt", "TRITON_TENSORRT_INCLUDE_PATHS", None, "c:/TensorRT/include" + ) + ) return cargs @@ -779,12 +858,9 @@ def install_dcgm_libraries(dcgm_version, target_machine): # Install DCGM. Steps from https://developer.nvidia.com/dcgm#Downloads RUN dnf config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel8/sbsa/cuda-rhel8.repo \\ && dnf clean expire-cache \\ - && dnf makecache --refresh \\ - && dnf install --assumeyes \\ - datacenter-gpu-manager-4-core-1:{} \\ - datacenter-gpu-manager-4-devel-1:{} + && dnf install -y datacenter-gpu-manager-{} """.format( - dcgm_version, dcgm_version, dcgm_version + dcgm_version, dcgm_version ) else: return """ @@ -792,12 +868,9 @@ def install_dcgm_libraries(dcgm_version, target_machine): # Install DCGM. Steps from https://developer.nvidia.com/dcgm#Downloads RUN dnf config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel8/x86_64/cuda-rhel8.repo \\ && dnf clean expire-cache \\ - && dnf makecache --refresh \\ - && dnf install --assumeyes \\ - datacenter-gpu-manager-4-core-1:{} \\ - datacenter-gpu-manager-4-devel-1:{} + && dnf install -y datacenter-gpu-manager-{} """.format( - dcgm_version, dcgm_version, dcgm_version + dcgm_version, dcgm_version ) else: if target_machine == "aarch64": @@ -808,12 +881,10 @@ def install_dcgm_libraries(dcgm_version, target_machine): https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/sbsa/cuda-keyring_1.1-1_all.deb \\ && apt install /tmp/cuda-keyring.deb \\ && rm /tmp/cuda-keyring.deb \\ - && apt update -qq \\ - && apt install --yes --no-install-recommends \\ - datacenter-gpu-manager-4-core=1:{} \\ - datacenter-gpu-manager-4-dev=1:{} + && apt-get update \\ + && apt-get install -y datacenter-gpu-manager=1:{} """.format( - dcgm_version, dcgm_version, dcgm_version + dcgm_version, dcgm_version ) else: return """ @@ -823,12 +894,10 @@ def install_dcgm_libraries(dcgm_version, target_machine): https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/x86_64/cuda-keyring_1.1-1_all.deb \\ && apt install /tmp/cuda-keyring.deb \\ && rm /tmp/cuda-keyring.deb \\ - && apt update -qq \\ - && apt install --yes --no-install-recommends \\ - datacenter-gpu-manager-4-core=1:{} \\ - datacenter-gpu-manager-4-dev=1:{} + && apt-get update \\ + && apt-get install -y datacenter-gpu-manager=1:{} """.format( - dcgm_version, dcgm_version, dcgm_version + dcgm_version, dcgm_version ) @@ -848,7 +917,7 @@ def create_dockerfile_buildbase_rhel(ddir, dockerfile_name, argmap): ARG TRITON_VERSION ARG TRITON_CONTAINER_VERSION -ENV PIP_BREAK_SYSTEM_PACKAGES=1 CMAKE_POLICY_VERSION_MINIMUM=3.5 +ENV PIP_BREAK_SYSTEM_PACKAGES=1 """ df += """ # Install docker docker buildx @@ -890,13 +959,6 @@ def create_dockerfile_buildbase_rhel(ddir, dockerfile_name, argmap): xz-devel \\ zlib-devel """ - if argmap["NVIDIA_BUILD_ID"] is not None: - df += """ -ENV BUILD_NUMBER={} -""".format( - argmap["NVIDIA_BUILD_ID"] - ) - if os.getenv("CCACHE_REMOTE_ONLY") and os.getenv("CCACHE_REMOTE_STORAGE"): df += """ RUN curl -k -s -L https://github.com/ccache/ccache/archive/refs/tags/v4.10.2.tar.gz -o /tmp/ccache.tar.gz \\ @@ -926,18 +988,25 @@ def create_dockerfile_buildbase_rhel(ddir, dockerfile_name, argmap): setuptools \\ docker \\ virtualenv \\ - patchelf==0.17.2 \\ - cmake==4.0.3 -""" - df += f""" + patchelf==0.17.2 + # Install boost version >= 1.78 for boost::span # Current libboost-dev apt packages are < 1.78, so install from tar.gz -RUN wget -O /tmp/boost.tar.gz {FLAGS.boost_url} \\ - && sha256sum /tmp/boost.tar.gz | grep {FLAGS.boost_sha256} \\ +RUN wget -O /tmp/boost.tar.gz \\ + https://archives.boost.io/release/1.80.0/source/boost_1_80_0.tar.gz \\ && (cd /tmp && tar xzf boost.tar.gz) \\ && mv /tmp/boost_1_80_0/boost /usr/include/boost -""" +# Server build requires recent version of CMake (FetchContent required) +# Might not need this if the installed version of cmake is high enough for our build. +# RUN apt update -q=2 \\ +# && apt install -y gpg wget \\ +# && wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null \\ +# && . /etc/os-release \\ +# && echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $UBUNTU_CODENAME main" | tee /etc/apt/sources.list.d/kitware.list >/dev/null \\ +# && apt-get update -q=2 \\ +# && apt-get install -y --no-install-recommends cmake=3.27.7* cmake-data=3.27.7* +""" if FLAGS.enable_gpu: df += install_dcgm_libraries(argmap["DCGM_VERSION"], target_machine()) df += """ @@ -972,15 +1041,17 @@ def create_dockerfile_buildbase(ddir, dockerfile_name, argmap): ARG TRITON_VERSION ARG TRITON_CONTAINER_VERSION -ENV PIP_BREAK_SYSTEM_PACKAGES=1 CMAKE_POLICY_VERSION_MINIMUM=3.5 +ENV PIP_BREAK_SYSTEM_PACKAGES=1 """ - if argmap["NVIDIA_BUILD_ID"] is not None: + # Install the windows- or linux-specific buildbase dependencies + if target_platform() == "windows": + df += """ +RUN python3 -m pip install build + +SHELL ["cmd", "/S", "/C"] +""" + else: df += """ -ENV BUILD_NUMBER={} -""".format( - argmap["NVIDIA_BUILD_ID"] - ) - df += """ # Ensure apt-get won't prompt for selecting options ENV DEBIAN_FRONTEND=noninteractive @@ -1037,22 +1108,27 @@ def create_dockerfile_buildbase(ddir, dockerfile_name, argmap): build \\ docker \\ virtualenv \\ - patchelf==0.17.2 \\ - cmake==4.0.3 \\ - pybind11[global] -""" + patchelf==0.17.2 - df += f""" # Install boost version >= 1.78 for boost::span # Current libboost-dev apt packages are < 1.78, so install from tar.gz -RUN wget -O /tmp/boost.tar.gz {FLAGS.boost_url} \\ - && sha256sum /tmp/boost.tar.gz | grep {FLAGS.boost_sha256} \\ +RUN wget -O /tmp/boost.tar.gz \\ + https://archives.boost.io/release/1.80.0/source/boost_1_80_0.tar.gz \\ && (cd /tmp && tar xzf boost.tar.gz) \\ && mv /tmp/boost_1_80_0/boost /usr/include/boost + +# Server build requires recent version of CMake (FetchContent required) +RUN apt update -q=2 \\ + && apt install -y gpg wget \\ + && wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null \\ + && . /etc/os-release \\ + && echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $UBUNTU_CODENAME main" | tee /etc/apt/sources.list.d/kitware.list >/dev/null \\ + && apt-get update -q=2 \\ + && apt-get install -y --no-install-recommends cmake=3.28.3* cmake-data=3.28.3* """ - if FLAGS.enable_gpu: - df += install_dcgm_libraries(argmap["DCGM_VERSION"], target_machine()) + if FLAGS.enable_gpu: + df += install_dcgm_libraries(argmap["DCGM_VERSION"], target_machine()) df += """ ENV TRITON_SERVER_VERSION ${TRITON_VERSION} @@ -1076,7 +1152,14 @@ def create_dockerfile_buildbase(ddir, dockerfile_name, argmap): # Copy in the triton source. We remove existing contents first in # case the FROM container has something there already. - df += """ + if target_platform() == "windows": + df += """ +WORKDIR /workspace +RUN rmdir /S/Q * || exit 0 +COPY . . +""" + else: + df += """ WORKDIR /workspace RUN rm -fr * COPY . . @@ -1123,19 +1206,12 @@ def create_dockerfile_linux( df = """ ARG TRITON_VERSION={} ARG TRITON_CONTAINER_VERSION={} +ARG BASE_IMAGE={} + """.format( argmap["TRITON_VERSION"], argmap["TRITON_CONTAINER_VERSION"], - ) - if "vllm" in backends and argmap["INFERENCE_IMAGE"] is None: - argmap[ - "INFERENCE_IMAGE" - ] = f"nvcr.io/nvidia/vllm:{FLAGS.upstream_container_version}-py3" - df += """ARG BASE_IMAGE={} -""".format( - argmap["INFERENCE_IMAGE"] - if argmap["INFERENCE_IMAGE"] is not None - else argmap["BASE_IMAGE"], + argmap["BASE_IMAGE"], ) # PyTorch backends need extra CUDA and other @@ -1165,16 +1241,16 @@ def create_dockerfile_linux( argmap, backends, FLAGS.enable_gpu, target_machine() ) - df += f""" + df += """ WORKDIR /opt -COPY build/install tritonserver +COPY --chown=1000:1000 build/install tritonserver WORKDIR /opt/tritonserver -COPY NVIDIA_Deep_Learning_Container_License.pdf . +COPY --chown=1000:1000 NVIDIA_Deep_Learning_Container_License.pdf . RUN find /opt/tritonserver/python -maxdepth 1 -type f -name \\ - "tritonserver-*.whl" | xargs -I {{}} pip install --upgrade {{}}[{FLAGS.triton_wheels_dependencies_group}] && \\ + "tritonserver-*.whl" | xargs -I {} pip install --upgrade {}[all] && \\ find /opt/tritonserver/python -maxdepth 1 -type f -name \\ - "tritonfrontend-*.whl" | xargs -I {{}} pip install --upgrade {{}}[{FLAGS.triton_wheels_dependencies_group}] + "tritonfrontend-*.whl" | xargs -I {} pip install --upgrade {}[all] RUN pip3 install -r python/openai/requirements.txt @@ -1185,20 +1261,36 @@ def create_dockerfile_linux( df += """ LABEL com.amazonaws.sagemaker.capabilities.accept-bind-to-port=true LABEL com.amazonaws.sagemaker.capabilities.multi-models=true -COPY docker/sagemaker/serve /usr/bin/. +COPY --chown=1000:1000 docker/sagemaker/serve /usr/bin/. """ # This is required since libcublasLt.so is not present during the build # stage of the PyTorch backend if not FLAGS.enable_gpu and ("pytorch" in backends): df += """ -RUN patchelf --add-needed /usr/local/cuda/lib64/stubs/libcublasLt.so.13 backends/pytorch/libtorch_cuda.so +RUN patchelf --add-needed /usr/local/cuda/lib64/stubs/libcublasLt.so.12 backends/pytorch/libtorch_cuda.so """ if "tensorrtllm" in backends: df += """ +# Install required packages for TRT-LLM models +# Remove contents that are not needed in runtime +# Setuptools has breaking changes in version 70.0.0, so fix it to 69.5.1 +# The generated code in grpc_service_pb2_grpc.py depends on grpcio>=1.64.0, so fix it to 1.64.0 RUN ldconfig && \\ - find /opt/tritonserver -name lib*so -exec dirname {} \\; > /etc/ld.so.conf.d/tritonserver.conf && \\ - ldconfig - + ARCH="$(uname -i)" && \\ + rm -fr ${TRT_ROOT}/bin ${TRT_ROOT}/targets/${ARCH}-linux-gnu/bin ${TRT_ROOT}/data && \\ + rm -fr ${TRT_ROOT}/doc ${TRT_ROOT}/onnx_graphsurgeon ${TRT_ROOT}/python && \\ + rm -fr ${TRT_ROOT}/samples ${TRT_ROOT}/targets/${ARCH}-linux-gnu/samples && \\ + pip3 install --no-cache-dir transformers && \\ + find /usr -name libtensorrt_llm.so -exec dirname {} \; > /etc/ld.so.conf.d/tensorrt-llm.conf && \\ + find /opt/tritonserver -name libtritonserver.so -exec dirname {} \; > /etc/ld.so.conf.d/triton-tensorrtllm-worker.conf && \\ + pip3 install --no-cache-dir grpcio-tools==1.64.0 && \\ + pip3 uninstall -y setuptools +ENV LD_LIBRARY_PATH=/usr/local/tensorrt/lib/:/opt/tritonserver/backends/tensorrtllm:$LD_LIBRARY_PATH + +# There are some ucc issues when spawning mpi processes with ompi v4.1.7a1. +# Downgrade to ompi v4.1.5rc2 to avoid the issue. +RUN rm -fr /opt/hpcx/ompi +COPY --from=nvcr.io/nvidia/tritonserver:24.02-py3-min /opt/hpcx/ompi /opt/hpcx/ompi """ with open(os.path.join(ddir, dockerfile_name), "w") as dfile: dfile.write(df) @@ -1248,9 +1340,8 @@ def dockerfile_prepare_container_linux(argmap, backends, enable_gpu, target_mach ENV TRITON_SERVER_GPU_ENABLED {gpu_enabled} # Create a user that can be used to run triton as -# non-root. Make sure that this user is given ID 1000. Server -# artifacts copied below remain owned by root; the triton-server -# user reads and executes them via standard group/other permissions. +# non-root. Make sure that this user to given ID 1000. All server +# artifacts copied below are assign to this user. ENV TRITON_SERVER_USER=triton-server RUN userdel tensorrt-server > /dev/null 2>&1 || true \\ && userdel ubuntu > /dev/null 2>&1 || true \\ @@ -1276,7 +1367,7 @@ def dockerfile_prepare_container_linux(argmap, backends, enable_gpu, target_mach libb64-devel \\ gperftools-devel \\ wget \\ - python3.12-pip \\ + python3-pip \\ numactl-devel RUN pip3 install patchelf==0.17.2 @@ -1301,6 +1392,7 @@ def dockerfile_prepare_container_linux(argmap, backends, enable_gpu, target_mach libgoogle-perftools-dev \\ libjemalloc-dev \\ libnuma-dev \\ + software-properties-common \\ wget \\ {backend_dependencies} \\ python3-pip \\ @@ -1378,14 +1470,40 @@ def dockerfile_prepare_container_linux(argmap, backends, enable_gpu, target_mach virtualenv \\ && rm -rf /var/lib/apt/lists/* """ - if "tensorrtllm" in backends or "vllm" in backends: + if "tensorrtllm" in backends: df += """ -ENV TRITON_CUDACRT_PATH=/usr/local/cuda/include \\ - TRITON_CUDART_PATH=/usr/local/cuda/include \\ - TRITON_CUOBJDUMP_PATH=/usr/local/cuda/bin/cuobjdump \\ - TRITON_CUPTI_PATH=/usr/local/cuda/include \\ - TRITON_NVDISASM_PATH=/usr/local/cuda/bin/nvdisasm \\ - TRITON_PTXAS_PATH=/usr/local/cuda/bin/ptxas +# Updating the openssh-client to fix for the CVE-2024-6387. This can be removed when trtllm uses a later CUDA container(12.5 or later) +RUN apt-get update \\ + && apt-get install -y --no-install-recommends \\ + openssh-client \\ + && rm -rf /var/lib/apt/lists/* + """ + + if "vllm" in backends: + df += f""" +ARG BUILD_PUBLIC_VLLM="true" +ARG VLLM_INDEX_URL +ARG PYTORCH_TRITON_URL + +RUN --mount=type=secret,id=req,target=/run/secrets/requirements \\ + if [ "$BUILD_PUBLIC_VLLM" = "false" ]; then \\ + pip3 install --no-cache-dir \\ + mkl==2021.1.1 \\ + mkl-include==2021.1.1 \\ + mkl-devel==2021.1.1 \\ + && pip3 install --no-cache-dir --progress-bar on --index-url $VLLM_INDEX_URL -r /run/secrets/requirements \\ + # Need to install in-house build of pytorch-triton to support triton_key definition used by torch 2.5.1 + && cd /tmp \\ + && wget $PYTORCH_TRITON_URL \\ + && pip install --no-cache-dir /tmp/pytorch_triton-*.whl \\ + && rm /tmp/pytorch_triton-*.whl; \\ + else \\ + # public vLLM needed for vLLM backend + pip3 install vllm=={DEFAULT_TRITON_VERSION_MAP["vllm_version"]}; \\ + fi + +ARG PYVER=3.12 +ENV LD_LIBRARY_PATH /usr/local/lib:/usr/local/lib/python${{PYVER}}/dist-packages/torch/lib:${{LD_LIBRARY_PATH}} """ if "dali" in backends: @@ -1394,18 +1512,6 @@ def dockerfile_prepare_container_linux(argmap, backends, enable_gpu, target_mach ENV PYTHONPATH=/opt/tritonserver/backends/dali/wheel/dali:$PYTHONPATH """ - if target_platform() == "rhel": - repo_arch = "sbsa" if target_machine == "aarch64" else "x86_64" - df += """ -RUN dnf config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel8/{repo_arch}/cuda-rhel8.repo \\ - && dnf clean expire-cache \\ - && dnf install --assumeyes libnvshmem3-cuda-13 - -RUN dirname $(find /usr -name "libcudart*.so" -o -name "libnvinf*.so" -o -name "libnvshm*" -type f) | sort -u > /etc/ld.so.conf.d/triton-cuda-libs.conf && ldconfig -""".format( - repo_arch=repo_arch - ) - df += """ WORKDIR /opt/tritonserver RUN rm -fr /opt/tritonserver/* @@ -1445,20 +1551,18 @@ def add_cpu_libs_to_linux_dockerfile(backends, target_machine): df += """ RUN mkdir -p /usr/local/cuda/lib64/stubs COPY --from=min_container /usr/local/cuda/lib64/stubs/libcusparse.so /usr/local/cuda/lib64/stubs/libcusparse.so.12 -COPY --from=min_container /usr/local/cuda/lib64/stubs/libcusolver.so /usr/local/cuda/lib64/stubs/libcusolver.so.12 +COPY --from=min_container /usr/local/cuda/lib64/stubs/libcusolver.so /usr/local/cuda/lib64/stubs/libcusolver.so.11 COPY --from=min_container /usr/local/cuda/lib64/stubs/libcurand.so /usr/local/cuda/lib64/stubs/libcurand.so.10 -COPY --from=min_container /usr/local/cuda/lib64/stubs/libcufft.so /usr/local/cuda/lib64/stubs/libcufft.so.12 -COPY --from=min_container /usr/local/cuda/lib64/stubs/libcublas.so /usr/local/cuda/lib64/stubs/libcublas.so.13 -COPY --from=min_container /usr/local/cuda/lib64/stubs/libcublasLt.so /usr/local/cuda/lib64/stubs/libcublasLt.so.13 +COPY --from=min_container /usr/local/cuda/lib64/stubs/libcufft.so /usr/local/cuda/lib64/stubs/libcufft.so.11 +COPY --from=min_container /usr/local/cuda/lib64/stubs/libcublas.so /usr/local/cuda/lib64/stubs/libcublas.so.12 +COPY --from=min_container /usr/local/cuda/lib64/stubs/libcublasLt.so /usr/local/cuda/lib64/stubs/libcublasLt.so.12 +COPY --from=min_container /usr/local/cuda/lib64/stubs/libcublasLt.so /usr/local/cuda/lib64/stubs/libcublasLt.so.11 RUN mkdir -p /usr/local/cuda/targets/{cuda_arch}-linux/lib -COPY --from=min_container /usr/local/cuda/lib64/libcudart.so.13 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. -COPY --from=min_container /usr/local/cuda/lib64/libcupti.so.13 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. -COPY --from=min_container /usr/local/cuda/lib64/libnvJitLink.so.13 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. -COPY --from=min_container /usr/local/cuda/lib64/libcufile.so.0 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. -COPY --from=min_container /usr/local/cuda/lib64/libnvrtc.so.13 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. -COPY --from=min_container /usr/local/cuda/lib64/libcusparseLt.so.0 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. -COPY --from=min_container /usr/local/cuda/lib64/libnvshmem_host.so.3 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. +COPY --from=min_container /usr/local/cuda/lib64/libcudart.so.12 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. +COPY --from=min_container /usr/local/cuda/lib64/libcupti.so.12 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. +COPY --from=min_container /usr/local/cuda/lib64/libnvToolsExt.so.1 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. +COPY --from=min_container /usr/local/cuda/lib64/libnvJitLink.so.12 /usr/local/cuda/targets/{cuda_arch}-linux/lib/. RUN mkdir -p /opt/hpcx/ucc/lib/ /opt/hpcx/ucx/lib/ COPY --from=min_container /opt/hpcx/ucc/lib/libucc.so.1 /opt/hpcx/ucc/lib/libucc.so.1 @@ -1518,6 +1622,55 @@ def change_default_python_version_rhel(version): return df +def create_dockerfile_windows( + ddir, dockerfile_name, argmap, backends, repoagents, caches +): + df = """ +ARG TRITON_VERSION={} +ARG TRITON_CONTAINER_VERSION={} +ARG BASE_IMAGE={} + +############################################################################ +## Production stage: Create container with just inference server executable +############################################################################ +FROM ${{BASE_IMAGE}} + +ARG TRITON_VERSION +ARG TRITON_CONTAINER_VERSION + +ENV TRITON_SERVER_VERSION ${{TRITON_VERSION}} +ENV NVIDIA_TRITON_SERVER_VERSION ${{TRITON_CONTAINER_VERSION}} +LABEL com.nvidia.tritonserver.version="${{TRITON_SERVER_VERSION}}" + +RUN setx path "%path%;C:\opt\tritonserver\bin" + +""".format( + argmap["TRITON_VERSION"], + argmap["TRITON_CONTAINER_VERSION"], + argmap["BASE_IMAGE"], + ) + df += """ +WORKDIR /opt +RUN rmdir /S/Q tritonserver || exit 0 +COPY --chown=1000:1000 build/install tritonserver + +WORKDIR /opt/tritonserver +COPY --chown=1000:1000 NVIDIA_Deep_Learning_Container_License.pdf . + +""" + df += """ +ENTRYPOINT [] +ENV NVIDIA_BUILD_ID {} +LABEL com.nvidia.build.id={} +LABEL com.nvidia.build.ref={} +""".format( + argmap["NVIDIA_BUILD_ID"], argmap["NVIDIA_BUILD_ID"], argmap["NVIDIA_BUILD_REF"] + ) + + with open(os.path.join(ddir, dockerfile_name), "w") as dfile: + dfile.write(df) + + def create_build_dockerfiles( container_build_dir, images, backends, repoagents, caches, endpoints ): @@ -1527,6 +1680,8 @@ def create_build_dockerfiles( print( "warning: RHEL is not an officially supported target and you will probably experience errors attempting to build this container." ) + elif target_platform() == "windows": + base_image = "mcr.microsoft.com/dotnet/framework/sdk:4.8" elif target_platform() == "rhel": raise KeyError("A base image must be specified when targeting RHEL") elif FLAGS.enable_gpu: @@ -1536,24 +1691,22 @@ def create_build_dockerfiles( else: base_image = "ubuntu:24.04" - if "inference" in images: - inference_image = images["inference"] - else: - inference_image = None - dockerfileargmap = { "NVIDIA_BUILD_REF": "" if FLAGS.build_sha is None else FLAGS.build_sha, "NVIDIA_BUILD_ID": "" if FLAGS.build_id is None else FLAGS.build_id, "TRITON_VERSION": FLAGS.version, "TRITON_CONTAINER_VERSION": FLAGS.container_version, "BASE_IMAGE": base_image, - "INFERENCE_IMAGE": inference_image, "DCGM_VERSION": FLAGS.dcgm_version, } # For CPU-only image we need to copy some cuda libraries and dependencies # since we are using PyTorch containers that are not CPU-only. - if not FLAGS.enable_gpu and ("pytorch" in backends): + if ( + not FLAGS.enable_gpu + and ("pytorch" in backends) + and (target_platform() != "windows") + ): if "gpu-base" in images: gpu_base_image = images["gpu-base"] else: @@ -1571,15 +1724,25 @@ def create_build_dockerfiles( FLAGS.build_dir, "Dockerfile.buildbase", dockerfileargmap ) - create_dockerfile_linux( - FLAGS.build_dir, - "Dockerfile", - dockerfileargmap, - backends, - repoagents, - caches, - endpoints, - ) + if target_platform() == "windows": + create_dockerfile_windows( + FLAGS.build_dir, + "Dockerfile", + dockerfileargmap, + backends, + repoagents, + caches, + ) + else: + create_dockerfile_linux( + FLAGS.build_dir, + "Dockerfile", + dockerfileargmap, + backends, + repoagents, + caches, + endpoints, + ) # Dockerfile used for the creating the CI base image. create_dockerfile_cibase(FLAGS.build_dir, "Dockerfile.cibase", dockerfileargmap) @@ -1621,7 +1784,14 @@ def create_docker_build_script(script_name, container_install_dir, container_ci_ "--pull", ] - baseargs += ["--cache-from={}".format(k) for k in cachefrommap] + # Windows docker runs in a VM and memory needs to be specified + # explicitly (at least for some configurations of docker). + if target_platform() == "windows": + if FLAGS.container_memory: + baseargs += ["--memory", FLAGS.container_memory] + + if target_platform() != "windows": + baseargs += ["--cache-from={}".format(k) for k in cachefrommap] baseargs += ["."] @@ -1654,24 +1824,35 @@ def create_docker_build_script(script_name, container_install_dir, container_ci_ if not FLAGS.no_container_interactive: runargs += ["-it"] - runargs += ["-v", "/var/run/docker.sock:/var/run/docker.sock"] - if FLAGS.use_user_docker_config: - if os.path.exists(FLAGS.use_user_docker_config): - runargs += [ - "-v", - os.path.expanduser( - FLAGS.use_user_docker_config + ":/root/.docker/config.json" - ), - ] + if target_platform() == "windows": + if FLAGS.container_memory: + runargs += ["--memory", FLAGS.container_memory] + runargs += ["-v", "\\\\.\pipe\docker_engine:\\\\.\pipe\docker_engine"] + else: + runargs += ["-v", "/var/run/docker.sock:/var/run/docker.sock"] + if FLAGS.use_user_docker_config: + if os.path.exists(FLAGS.use_user_docker_config): + runargs += [ + "-v", + os.path.expanduser( + FLAGS.use_user_docker_config + ":/root/.docker/config.json" + ), + ] runargs += ["tritonserver_buildbase"] - runargs += ["./cmake_build"] + if target_platform() == "windows": + runargs += ["powershell.exe", "-noexit", "-File", "./cmake_build.ps1"] + else: + runargs += ["./cmake_build"] # Remove existing tritonserver_builder container... - docker_script._file.write( - 'if [ "$(docker ps -a | grep tritonserver_builder)" ]; then docker rm -f tritonserver_builder; fi\n' - ) + if target_platform() == "windows": + docker_script.cmd(["docker", "rm", "tritonserver_builder"]) + else: + docker_script._file.write( + 'if [ "$(docker ps -a | grep tritonserver_builder)" ]; then docker rm -f tritonserver_builder; fi\n' + ) docker_script.cmd(runargs, check_exitcode=True) @@ -1709,9 +1890,8 @@ def create_docker_build_script(script_name, container_install_dir, container_ci_ if secrets: finalargs += [ f"--secret id=req,src={requirements}", - f"--secret id=VLLM_INDEX_URL", - f"--secret id=PYTORCH_TRITON_URL", - f"--secret id=NVPL_SLIM_URL", + f"--build-arg VLLM_INDEX_URL={vllm_index_url}", + f"--build-arg PYTORCH_TRITON_URL={pytorch_triton_url}", f"--build-arg BUILD_PUBLIC_VLLM={build_public_vllm}", ] finalargs += [ @@ -1762,7 +1942,21 @@ def core_build( ) cmake_script.makeinstall() - if target_platform() == "rhel": + if target_platform() == "windows": + cmake_script.mkdir(os.path.join(install_dir, "bin")) + cmake_script.cp( + os.path.join(repo_install_dir, "bin", "tritonserver.exe"), + os.path.join(install_dir, "bin"), + ) + cmake_script.cp( + os.path.join(repo_install_dir, "bin", "tritonserver.dll"), + os.path.join(install_dir, "bin"), + ) + cmake_script.cp( + os.path.join(repo_install_dir, "lib", "tritonserver.lib"), + os.path.join(install_dir, "bin"), + ) + elif target_platform() == "rhel": cmake_script.mkdir(os.path.join(install_dir, "bin")) cmake_script.cp( os.path.join(repo_install_dir, "bin", "tritonserver"), @@ -1805,22 +1999,25 @@ def core_build( cmake_script.cp(os.path.join(repo_dir, "LICENSE"), install_dir) cmake_script.cp(os.path.join(repo_dir, "TRITON_VERSION"), install_dir) - # If requested, package the source code for all OSS used to build. - if ( - (not FLAGS.no_container_build) - and (not FLAGS.no_core_build) - and (not FLAGS.no_container_source) - ): - cmake_script.mkdir(os.path.join(install_dir, "third-party-src")) - cmake_script.cwd(repo_build_dir) - cmake_script.tar( - "third-party-src", - os.path.join(install_dir, "third-party-src", "src.tar.gz"), - ) - cmake_script.cp( - os.path.join(repo_dir, "docker", "README.third-party-src"), - os.path.join(install_dir, "third-party-src", "README"), - ) + # If requested, package the source code for all OSS used to build + # For windows, Triton is not delivered as a container so skip for + # windows platform. + if target_platform() != "windows": + if ( + (not FLAGS.no_container_build) + and (not FLAGS.no_core_build) + and (not FLAGS.no_container_source) + ): + cmake_script.mkdir(os.path.join(install_dir, "third-party-src")) + cmake_script.cwd(repo_build_dir) + cmake_script.tar( + "third-party-src", + os.path.join(install_dir, "third-party-src", "src.tar.gz"), + ) + cmake_script.cp( + os.path.join(repo_dir, "docker", "README.third-party-src"), + os.path.join(install_dir, "third-party-src", "README"), + ) cmake_script.comment() cmake_script.comment("end Triton core library and tritonserver executable") @@ -1873,11 +2070,7 @@ def backend_build( cmake_script.comment() cmake_script.mkdir(build_dir) cmake_script.cwd(build_dir) - if be == "tensorrtllm": - repository_name = "TensorRT-LLM" - cmake_script.gitclone(repository_name, tag, be, github_organization) - else: - cmake_script.gitclone(backend_repo(be), tag, be, github_organization) + cmake_script.gitclone(backend_repo(be), tag, be, github_organization) if be == "tensorrtllm": tensorrtllm_prebuild(cmake_script) @@ -2030,6 +2223,11 @@ def cibase_build( cmake_script.mkdir(ci_dir) + # On windows we are not yet using a CI/QA docker image for + # testing, so don't do anything... + if target_platform() == "windows": + return + # The core build produces some artifacts that are needed for CI # testing, so include those in the install. cmake_script.cpdir(os.path.join(repo_dir, "qa"), ci_dir) @@ -2058,9 +2256,12 @@ def cibase_build( cmake_script.mkdir(os.path.join(ci_dir, "backends")) for be in ("identity", "repeat", "square"): be_install_dir = os.path.join(build_dir, be, "install", "backends", be) - cmake_script.cmd(f"if [[ -e {be_install_dir} ]]; then") + if target_platform() == "windows": + cmake_script.cmd(f"if (Test-Path -Path {be_install_dir}) {{") + else: + cmake_script.cmd(f"if [[ -e {be_install_dir} ]]; then") cmake_script.cpdir(be_install_dir, os.path.join(ci_dir, "backends")) - cmake_script.cmd("fi") + cmake_script.cmd("}" if target_platform() == "windows" else "fi") # Some of the unit-test built backends are needed for CI testing cmake_script.mkdir(os.path.join(ci_dir, "tritonbuild", "tritonserver", "backends")) @@ -2073,12 +2274,15 @@ def cibase_build( "iterative_sequence", ): be_install_dir = os.path.join(repo_install_dir, "backends", be) - cmake_script.cmd(f"if [[ -e {be_install_dir} ]]; then") + if target_platform() == "windows": + cmake_script.cmd(f"if (Test-Path -Path {be_install_dir}) {{") + else: + cmake_script.cmd(f"if [[ -e {be_install_dir} ]]; then") cmake_script.cpdir( be_install_dir, os.path.join(ci_dir, "tritonbuild", "tritonserver", "backends"), ) - cmake_script.cmd("fi") + cmake_script.cmd("}" if target_platform() == "windows" else "fi") # The onnxruntime_backend build produces some artifacts that # are needed for CI testing. @@ -2116,37 +2320,57 @@ def cibase_build( def finalize_build(cmake_script, install_dir, ci_dir): - cmake_script.cmd(f"chmod -R u+rwX,go+rX,go-w {install_dir}") - cmake_script.cmd(f"chmod -R u+rwX,go+rX,go-w {ci_dir}") + cmake_script.cmd(f"chmod -R a+rw {install_dir}") + cmake_script.cmd(f"chmod -R a+rw {ci_dir}") def enable_all(): - all_backends = [ - "ensemble", - "identity", - "square", - "repeat", - "onnxruntime", - "python", - "dali", - "pytorch", - "openvino", - "fil", - "tensorrt", - ] - all_repoagents = ["checksum"] - all_caches = ["local", "redis"] - all_filesystems = ["gcs", "s3", "azure_storage"] - all_endpoints = ["http", "grpc", "sagemaker", "vertex-ai"] - - FLAGS.enable_logging = True - FLAGS.enable_stats = True - FLAGS.enable_metrics = True - FLAGS.enable_gpu_metrics = True - FLAGS.enable_cpu_metrics = True - FLAGS.enable_tracing = True - FLAGS.enable_nvtx = True - FLAGS.enable_gpu = True + if target_platform() != "windows": + all_backends = [ + "ensemble", + "identity", + "square", + "repeat", + "onnxruntime", + "python", + "dali", + "pytorch", + "openvino", + "fil", + "tensorrt", + ] + all_repoagents = ["checksum"] + all_caches = ["local", "redis"] + all_filesystems = ["gcs", "s3", "azure_storage"] + all_endpoints = ["http", "grpc", "sagemaker", "vertex-ai"] + + FLAGS.enable_logging = True + FLAGS.enable_stats = True + FLAGS.enable_metrics = True + FLAGS.enable_gpu_metrics = True + FLAGS.enable_cpu_metrics = True + FLAGS.enable_tracing = True + FLAGS.enable_nvtx = True + FLAGS.enable_gpu = True + else: + all_backends = [ + "ensemble", + "identity", + "square", + "repeat", + "onnxruntime", + "openvino", + "tensorrt", + ] + all_repoagents = ["checksum"] + all_caches = ["local", "redis"] + all_filesystems = [] + all_endpoints = ["http", "grpc"] + + FLAGS.enable_logging = True + FLAGS.enable_stats = True + FLAGS.enable_tracing = True + FLAGS.enable_gpu = True requested_backends = [] for be in FLAGS.backend: @@ -2230,11 +2454,17 @@ def enable_all(): required=False, help="Do not use Docker --pull argument when building container.", ) + parser.add_argument( + "--container-memory", + default=None, + required=False, + help="Value for Docker --memory argument. Used only for windows builds.", + ) parser.add_argument( "--target-platform", required=False, default=None, - help='Target platform for build, can be "linux", "rhel" or "igpu". If not specified, build targets the current platform.', + help='Target platform for build, can be "linux", "rhel", "windows" or "igpu". If not specified, build targets the current platform.', ) parser.add_argument( "--target-machine", @@ -2508,6 +2738,12 @@ def enable_all(): default=DEFAULT_TRITON_VERSION_MAP["dcgm_version"], help="This flag sets the DCGM version for Triton Inference Server to be built. Default: the latest supported version.", ) + parser.add_argument( + "--vllm-version", + required=False, + default=DEFAULT_TRITON_VERSION_MAP["vllm_version"], + help="This flag sets the vLLM version for Triton Inference Server to be built. Default: the latest supported version.", + ) parser.add_argument( "--rhel-py-version", required=False, @@ -2522,16 +2758,11 @@ def enable_all(): metavar=("key", "value"), help="Add build secrets in the form of . These secrets are used during the build process for vllm. The secrets are passed to the Docker build step as `--secret id=`. The following keys are expected and their purposes are described below:\n\n" " - 'req': A file containing a list of dependencies for pip (e.g., requirements.txt).\n" + " - 'vllm_index_url': The index URL for the pip install.\n" + " - 'pytorch_triton_url': The location of the PyTorch wheel to download.\n" " - 'build_public_vllm': A flag (default is 'true') indicating whether to build the public VLLM version.\n\n" "Ensure that the required environment variables for these secrets are set before running the build.", ) - parser.add_argument( - "--triton-wheels-dependencies-group", - required=False, - type=str, - default="all", - help="The group of dependencies for Triton wheels to be installed. Default value is 'all'.", - ) FLAGS = parser.parse_args() if FLAGS.image is None: @@ -2561,13 +2792,6 @@ def enable_all(): if FLAGS.build_secret is None: FLAGS.build_secret = [] - FLAGS.boost_url = os.getenv( - "TRITON_BOOST_URL", - "https://archives.boost.io/release/1.80.0/source/boost_1_80_0.tar.gz", - ) - FLAGS.boost_sha256 = ( - "4b2136f98bdd1f5857f1c3dea9ac2018effe65286cf251534b6ae20cc45e1847" - ) # if --enable-all is specified, then update FLAGS to enable all # settings, backends, repo-agents, caches, file systems, endpoints, etc. if FLAGS.enable_all: @@ -2614,11 +2838,12 @@ def enable_all(): # backends, repo-agents, and caches if a repo-tag is not given # explicitly. For release branches we use the release branch as # the default, otherwise we use 'main'. - default_repo_tag = ( - "main" - if FLAGS.triton_container_version.endswith("dev") - else "r" + FLAGS.triton_container_version - ) + default_repo_tag = "main" + cver = FLAGS.upstream_container_version + if cver is None: + cver = FLAGS.triton_container_version + if not cver.endswith("dev"): + default_repo_tag = "r" + cver log("default repo-tag: {}".format(default_repo_tag)) # For other versions use the TRITON_VERSION_MAP unless explicitly @@ -2656,6 +2881,8 @@ def enable_all(): secrets = dict(getattr(FLAGS, "build_secret", [])) if secrets: requirements = secrets.get("req", "") + vllm_index_url = secrets.get("vllm_index_url", "") + pytorch_triton_url = secrets.get("pytorch_triton_url", "") build_public_vllm = secrets.get("build_public_vllm", "true") log('Build Arg for BUILD_PUBLIC_VLLM: "{}"'.format(build_public_vllm)) @@ -2685,7 +2912,7 @@ def enable_all(): len(parts) != 2, "--image must specify ," ) fail_if( - parts[0] not in ["base", "gpu-base", "pytorch", "inference"], + parts[0] not in ["base", "gpu-base", "pytorch"], "unsupported value for --image", ) log('image "{}": "{}"'.format(parts[0], parts[1])) @@ -2788,12 +3015,21 @@ def enable_all(): script_install_dir = script_ci_dir = FLAGS.install_dir script_cmake_dir = FLAGS.cmake_dir if not FLAGS.no_container_build: - script_build_dir = os.path.normpath(os.path.join(FLAGS.tmp_dir, "tritonbuild")) + # FLAGS.tmp_dir may be specified with "\" on Windows, adjust + # to "/" for docker usage. + script_build_dir = os.path.normpath( + os.path.join(FLAGS.tmp_dir, "tritonbuild").replace("\\", "/") + ) script_install_dir = os.path.normpath(os.path.join(script_build_dir, "install")) script_ci_dir = os.path.normpath(os.path.join(script_build_dir, "ci")) - script_repo_dir = script_cmake_dir = "/workspace" + if target_platform() == "windows": + script_repo_dir = script_cmake_dir = os.path.normpath("c:/workspace") + else: + script_repo_dir = script_cmake_dir = "/workspace" script_name = "cmake_build" + if target_platform() == "windows": + script_name += ".ps1" # Write the build script that invokes cmake for the core, backends, repo-agents, and caches. pathlib.Path(FLAGS.build_dir).mkdir(parents=True, exist_ok=True) @@ -2894,7 +3130,8 @@ def enable_all(): # written to the build-dir while running the docker container # may have root ownership, so give them permissions to be # managed by all users on the host system. - finalize_build(cmake_script, script_install_dir, script_ci_dir) + if target_platform() != "windows": + finalize_build(cmake_script, script_install_dir, script_ci_dir) # If --no-container-build is not specified then we perform the # actual build within a docker container and from that create the @@ -2903,6 +3140,8 @@ def enable_all(): # the build process. if not FLAGS.no_container_build: script_name = "docker_build" + if target_platform() == "windows": + script_name += ".ps1" create_build_dockerfiles( script_build_dir, images, backends, repoagents, caches, FLAGS.endpoint @@ -2913,6 +3152,12 @@ def enable_all(): # container-based build is requested use 'docker_build' script, # otherwise build directly on this system using cmake script. if not FLAGS.dryrun: - p = subprocess.Popen([f"./{script_name}"], cwd=FLAGS.build_dir) + if target_platform() == "windows": + p = subprocess.Popen( + ["powershell.exe", "-noexit", "-File", f"./{script_name}"], + cwd=FLAGS.build_dir, + ) + else: + p = subprocess.Popen([f"./{script_name}"], cwd=FLAGS.build_dir) p.wait() fail_if(p.returncode != 0, "build failed") diff --git a/compose.py b/compose.py index ef1342c0fe..88c6b15bf3 100755 --- a/compose.py +++ b/compose.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -98,12 +98,12 @@ def start_dockerfile(ddir, images, argmap, dockerfile_name, backends): # Copy over files df += """ WORKDIR /opt/tritonserver -COPY --from=full /opt/tritonserver/LICENSE . -COPY --from=full /opt/tritonserver/TRITON_VERSION . -COPY --from=full /opt/tritonserver/NVIDIA_Deep_Learning_Container_License.pdf . -COPY --from=full /opt/tritonserver/bin bin/ -COPY --from=full /opt/tritonserver/lib lib/ -COPY --from=full /opt/tritonserver/include include/ +COPY --chown=1000:1000 --from=full /opt/tritonserver/LICENSE . +COPY --chown=1000:1000 --from=full /opt/tritonserver/TRITON_VERSION . +COPY --chown=1000:1000 --from=full /opt/tritonserver/NVIDIA_Deep_Learning_Container_License.pdf . +COPY --chown=1000:1000 --from=full /opt/tritonserver/bin bin/ +COPY --chown=1000:1000 --from=full /opt/tritonserver/lib lib/ +COPY --chown=1000:1000 --from=full /opt/tritonserver/include include/ """ with open(os.path.join(ddir, dockerfile_name), "w") as dfile: dfile.write(df) @@ -112,10 +112,15 @@ def start_dockerfile(ddir, images, argmap, dockerfile_name, backends): def add_requested_backends(ddir, dockerfile_name, backends): df = "# Copying over backends \n" for backend in backends: - df += """COPY --from=full /opt/tritonserver/backends/{} /opt/tritonserver/backends/{} + df += """COPY --chown=1000:1000 --from=full /opt/tritonserver/backends/{} /opt/tritonserver/backends/{} """.format( backend, backend ) + if len(backends) > 0: + df += """ +# Top-level /opt/tritonserver/backends not copied so need to explicitly set permissions here +RUN chown triton-server:triton-server /opt/tritonserver/backends +""" with open(os.path.join(ddir, dockerfile_name), "a") as dfile: dfile.write(df) @@ -123,10 +128,15 @@ def add_requested_backends(ddir, dockerfile_name, backends): def add_requested_repoagents(ddir, dockerfile_name, repoagents): df = "# Copying over repoagents \n" for ra in repoagents: - df += """COPY --from=full /opt/tritonserver/repoagents/{} /opt/tritonserver/repoagents/{} + df += """COPY --chown=1000:1000 --from=full /opt/tritonserver/repoagents/{} /opt/tritonserver/repoagents/{} """.format( ra, ra ) + if len(repoagents) > 0: + df += """ +# Top-level /opt/tritonserver/repoagents not copied so need to explicitly set permissions here +RUN chown triton-server:triton-server /opt/tritonserver/repoagents +""" with open(os.path.join(ddir, dockerfile_name), "a") as dfile: dfile.write(df) @@ -134,10 +144,15 @@ def add_requested_repoagents(ddir, dockerfile_name, repoagents): def add_requested_caches(ddir, dockerfile_name, caches): df = "# Copying over caches \n" for cache in caches: - df += """COPY --from=full /opt/tritonserver/caches/{} /opt/tritonserver/caches/{} + df += """COPY --chown=1000:1000 --from=full /opt/tritonserver/caches/{} /opt/tritonserver/caches/{} """.format( cache, cache ) + if len(caches) > 0: + df += """ +# Top-level /opt/tritonserver/caches not copied so need to explicitly set permissions here +RUN chown triton-server:triton-server /opt/tritonserver/caches +""" with open(os.path.join(ddir, dockerfile_name), "a") as dfile: dfile.write(df) @@ -148,7 +163,7 @@ def end_dockerfile(ddir, dockerfile_name, argmap): if argmap["SAGEMAKER_ENDPOINT"]: df += """ LABEL com.amazonaws.sagemaker.capabilities.accept-bind-to-port=true -COPY --from=full /usr/bin/serve /usr/bin/. +COPY --chown=1000:1000 --from=full /usr/bin/serve /usr/bin/. """ with open(os.path.join(ddir, dockerfile_name), "a") as dfile: dfile.write(df) @@ -283,7 +298,7 @@ def create_argmap(images, skip_pull): dcgm_ver = re.search("DCGM_VERSION=([\S]{4,}) ", vars) dcgm_version = "" if dcgm_ver is None: - dcgm_version = "4.5.3-1" + dcgm_version = "3.3.6" log( "WARNING: DCGM version not found from image, installing the earlierst version {}".format( dcgm_version diff --git a/deploy/aws/values.yaml b/deploy/aws/values.yaml index c94f832aa8..d7b424435c 100644 --- a/deploy/aws/values.yaml +++ b/deploy/aws/values.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2026, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:26.05-py3 + imageName: nvcr.io/nvidia/tritonserver:25.03-py3 pullPolicy: IfNotPresent modelRepositoryPath: s3://triton-inference-server-repository/model_repository numGpus: 1 diff --git a/deploy/fleetcommand/Chart.yaml b/deploy/fleetcommand/Chart.yaml index bd360e7955..481c4bd44c 100644 --- a/deploy/fleetcommand/Chart.yaml +++ b/deploy/fleetcommand/Chart.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2026, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -26,7 +26,7 @@ apiVersion: v1 # appVersion is the Triton version; update when changing release -appVersion: 2.69.0 +appVersion: "2.56.0" description: Triton Inference Server (Fleet Command) name: triton-inference-server # version is the Chart version; update when changing anything in the chart diff --git a/deploy/fleetcommand/values.yaml b/deploy/fleetcommand/values.yaml index b911db4afd..a8458c97dc 100644 --- a/deploy/fleetcommand/values.yaml +++ b/deploy/fleetcommand/values.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2026, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:26.05-py3 + imageName: nvcr.io/nvidia/tritonserver:25.03-py3 pullPolicy: IfNotPresent numGpus: 1 serverCommand: tritonserver @@ -47,13 +47,13 @@ image: # # To set model control mode, uncomment and configure below # TODO: Fix the following url, it is invalid - # See https://github.com/triton-inference-server/server/blob/r26.05/docs/user_guide/model_management.md + # See https://github.com/triton-inference-server/server/blob/r25.03/docs/model_management.md # for more details #- --model-control-mode=explicit|poll|none # # Additional server args # - # see https://github.com/triton-inference-server/server/blob/r26.05/README.md + # see https://github.com/triton-inference-server/server/blob/r25.03/README.md # for more details service: diff --git a/deploy/gcp/values.yaml b/deploy/gcp/values.yaml index 9784c9d252..eb0a18b32f 100644 --- a/deploy/gcp/values.yaml +++ b/deploy/gcp/values.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2026, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -27,7 +27,7 @@ replicaCount: 1 image: - imageName: nvcr.io/nvidia/tritonserver:26.05-py3 + imageName: nvcr.io/nvidia/tritonserver:25.03-py3 pullPolicy: IfNotPresent modelRepositoryPath: gs://triton-inference-server-repository/model_repository numGpus: 1 diff --git a/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml b/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml index 0e1347f4fd..1671b334c8 100644 --- a/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml +++ b/deploy/gke-marketplace-app/benchmark/perf-analyzer-script/triton_client.yaml @@ -1,4 +1,4 @@ -# Copyright 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -33,7 +33,7 @@ metadata: namespace: default spec: containers: - - image: nvcr.io/nvidia/tritonserver:26.05-py3-sdk + - image: nvcr.io/nvidia/tritonserver:25.03-py3-sdk imagePullPolicy: Always name: nv-triton-client securityContext: diff --git a/deploy/gke-marketplace-app/server-deployer/build_and_push.sh b/deploy/gke-marketplace-app/server-deployer/build_and_push.sh index 4b4468d89d..31af7064f5 100755 --- a/deploy/gke-marketplace-app/server-deployer/build_and_push.sh +++ b/deploy/gke-marketplace-app/server-deployer/build_and_push.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -27,9 +27,9 @@ export REGISTRY=gcr.io/$(gcloud config get-value project | tr ':' '/') export APP_NAME=tritonserver -export MAJOR_VERSION=2.67 -export MINOR_VERSION=2.69.0 -export NGC_VERSION=26.05-py3 +export MAJOR_VERSION=2.56 +export MINOR_VERSION=2.56.0 +export NGC_VERSION=25.03-py3 docker pull nvcr.io/nvidia/$APP_NAME:$NGC_VERSION diff --git a/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml b/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml index d150f0e8d7..1d490f7c9c 100644 --- a/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml +++ b/deploy/gke-marketplace-app/server-deployer/chart/triton/Chart.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -25,7 +25,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. apiVersion: v1 -appVersion: "2.68" +appVersion: "2.56" description: Triton Inference Server name: triton-inference-server -version: 2.69.0 +version: 2.56.0 diff --git a/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml b/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml index 362107e71a..d97d89f04d 100644 --- a/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml +++ b/deploy/gke-marketplace-app/server-deployer/chart/triton/values.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -31,14 +31,14 @@ maxReplicaCount: 3 tritonProtocol: HTTP # HPA GPU utilization autoscaling target HPATargetAverageValue: 85 -modelRepositoryPath: gs://triton_sample_models/26.05 -publishedVersion: '2.69.0' +modelRepositoryPath: gs://triton_sample_models/25.03 +publishedVersion: '2.56.0' gcpMarketplace: true image: registry: gcr.io repository: nvidia-ngc-public/tritonserver - tag: 26.05-py3 + tag: 25.03-py3 pullPolicy: IfNotPresent # modify the model repository here to match your GCP storage bucket numGpus: 1 diff --git a/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml b/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml index 4c312c9880..7c5f5cc425 100644 --- a/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml +++ b/deploy/gke-marketplace-app/server-deployer/data-test/schema.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -27,7 +27,7 @@ x-google-marketplace: schemaVersion: v2 applicationApiVersion: v1beta1 - publishedVersion: '2.69.0' + publishedVersion: '2.56.0' publishedVersionMetadata: releaseNote: >- Initial release. @@ -88,11 +88,11 @@ properties: default: 85 modelRepositoryPath: type: string - title: Bucket where models are stored. Please make sure the user/service account to create the GKE app has permission to this GCS bucket. Read Triton documentation on configs and formatting details, supporting TensorRT, Pytorch, Onnx ... etc. + title: Bucket where models are stored. Please make sure the user/service account to create the GKE app has permission to this GCS bucket. Read Triton documentation on configs and formatting details, supporting TensorRT, TensorFlow, Pytorch, Onnx ... etc. default: gs://triton_sample_models/models image.ldPreloadPath: type: string - title: Leave this empty by default. Triton allows users to create custom layers for backend such as TensorRT plugin or custom ops, the compiled shared library must be provided via LD_PRELOAD environment variable. + title: Leave this empty by default. Triton allows users to create custom layers for backend such as TensorRT plugin or Tensorflow custom ops, the compiled shared library must be provided via LD_PRELOAD environment variable. default: '' image.logVerboseLevel: type: integer diff --git a/deploy/gke-marketplace-app/server-deployer/schema.yaml b/deploy/gke-marketplace-app/server-deployer/schema.yaml index ccf3b157c4..2569628502 100644 --- a/deploy/gke-marketplace-app/server-deployer/schema.yaml +++ b/deploy/gke-marketplace-app/server-deployer/schema.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -27,7 +27,7 @@ x-google-marketplace: schemaVersion: v2 applicationApiVersion: v1beta1 - publishedVersion: '2.69.0' + publishedVersion: '2.56.0' publishedVersionMetadata: releaseNote: >- Initial release. @@ -89,10 +89,10 @@ properties: modelRepositoryPath: type: string title: Bucket where models are stored. Please make sure the user/service account to create the GKE app has permission to this GCS bucket. Read Triton documentation on configs and formatting details, supporting TensorRT, TensorFlow, Pytorch, Onnx ... etc. - default: gs://triton_sample_models/26.05 + default: gs://triton_sample_models/25.03 image.ldPreloadPath: type: string - title: Leave this empty by default. Triton allows users to create custom layers for backend such as TensorRT plugin, the compiled shared library must be provided via LD_PRELOAD environment variable. + title: Leave this empty by default. Triton allows users to create custom layers for backend such as TensorRT plugin or Tensorflow custom ops, the compiled shared library must be provided via LD_PRELOAD environment variable. default: '' image.logVerboseLevel: type: integer diff --git a/deploy/gke-marketplace-app/trt-engine/README.md b/deploy/gke-marketplace-app/trt-engine/README.md index fff7466da4..8b3e284fdd 100644 --- a/deploy/gke-marketplace-app/trt-engine/README.md +++ b/deploy/gke-marketplace-app/trt-engine/README.md @@ -1,5 +1,5 @@ +# OpenAI-Compatible Frontend for Triton Inference Server (Beta) + +> [!NOTE] +> The OpenAI-Compatible API is currently in BETA. Its features and functionality +> are subject to change as we collect feedback. We're excited to hear any thoughts +> you have and what features you'd like to see! + +## Pre-requisites + +1. Docker + NVIDIA Container Runtime +2. A correctly configured `HF_TOKEN` for access to HuggingFace models. + - The current examples and testing primarily use the + [`meta-llama/Meta-Llama-3.1-8B-Instruct`](https://huggingface.co/meta-llama/Meta-Llama-3.1-8B-Instruct) + model, but you can manually bring your own models and adjust accordingly. + +## VLLM + +1. Launch the container and install dependencies: + - Mounts the `~/.huggingface/cache` for re-use of downloaded models across runs, containers, etc. + - Sets the [`HF_TOKEN`](https://huggingface.co/docs/huggingface_hub/en/package_reference/environment_variables#hftoken) environment variable to + access gated models, make sure this is set in your local environment if needed. + +```bash +docker run -it --net=host --gpus all --rm \ + -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ + -e HF_TOKEN \ + nvcr.io/nvidia/tritonserver:25.03-vllm-python-py3 +``` + +2. Launch the OpenAI-compatible Triton Inference Server: +```bash +cd /opt/tritonserver/python/openai + +# NOTE: Adjust the --tokenizer based on the model being used +python3 openai_frontend/main.py --model-repository tests/vllm_models --tokenizer meta-llama/Meta-Llama-3.1-8B-Instruct +``` + +
+Example output + +``` +... ++-----------------------+---------+--------+ +| Model | Version | Status | ++-----------------------+---------+--------+ +| llama-3.1-8b-instruct | 1 | READY | <- Correct Model Loaded in Triton ++-----------------------+---------+--------+ +... +Found model: name='llama-3.1-8b-instruct', backend='vllm' +[WARNING] Adding CORS for the following origins: ['http://localhost'] +INFO: Started server process [126] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Uvicorn running on http://0.0.0.0:9000 (Press CTRL+C to quit) <- OpenAI Frontend Started Successfully +``` + +
+ +3. Send a `/v1/chat/completions` request: + - Note the use of `jq` is optional, but provides a nicely formatted output for JSON responses. +```bash +MODEL="llama-3.1-8b-instruct" +curl -s http://localhost:9000/v1/chat/completions -H 'Content-Type: application/json' -d '{ + "model": "'${MODEL}'", + "messages": [{"role": "user", "content": "Say this is a test!"}] +}' | jq +``` + +
+Example output + +```json +{ + "id": "cmpl-6930b296-7ef8-11ef-bdd1-107c6149ca79", + "choices": [ + { + "finish_reason": "stop", + "index": 0, + "message": + { + "content": "This is only a test.", + "tool_calls": null, + "role": "assistant", + "function_call": null + }, + "logprobs": null + } + ], + "created": 1727679085, + "model": "llama-3.1-8b-instruct", + "system_fingerprint": null, + "object": "chat.completion", + "usage": null +} +``` + +
+ +4. Send a `/v1/completions` request: + - Note the use of `jq` is optional, but provides a nicely formatted output for JSON responses. +```bash +MODEL="llama-3.1-8b-instruct" +curl -s http://localhost:9000/v1/completions -H 'Content-Type: application/json' -d '{ + "model": "'${MODEL}'", + "prompt": "Machine learning is" +}' | jq +``` + +
+Example output + +```json +{ + "id": "cmpl-d51df75c-7ef8-11ef-bdd1-107c6149ca79", + "choices": [ + { + "finish_reason": "stop", + "index": 0, + "logprobs": null, + "text": " a field of computer science that focuses on developing algorithms that allow computers to learn from" + } + ], + "created": 1727679266, + "model": "llama-3.1-8b-instruct", + "system_fingerprint": null, + "object": "text_completion", + "usage": null +} +``` + +
+ +5. Benchmark with `genai-perf`: +- To install genai-perf in this container, see the instructions [here](https://github.com/triton-inference-server/perf_analyzer/tree/main/genai-perf#install-perf-analyzer-ubuntu-python-38) +- Or try using genai-perf from the [SDK container](https://github.com/triton-inference-server/perf_analyzer/tree/main/genai-perf#install-perf-analyzer-ubuntu-python-38) + +```bash +MODEL="llama-3.1-8b-instruct" +TOKENIZER="meta-llama/Meta-Llama-3.1-8B-Instruct" +genai-perf profile \ + --model ${MODEL} \ + --tokenizer ${TOKENIZER} \ + --service-kind openai \ + --endpoint-type chat \ + --url localhost:9000 \ + --streaming +``` + +
+Example output + +``` +2024-10-14 22:43 [INFO] genai_perf.parser:82 - Profiling these models: llama-3.1-8b-instruct +2024-10-14 22:43 [INFO] genai_perf.wrapper:163 - Running Perf Analyzer : 'perf_analyzer -m llama-3.1-8b-instruct --async --input-data artifacts/llama-3.1-8b-instruct-openai-chat-concurrency1/inputs.json -i http --concurrency-range 1 --endpoint v1/chat/completions --service-kind openai -u localhost:9000 --measurement-interval 10000 --stability-percentage 999 --profile-export-file artifacts/llama-3.1-8b-instruct-openai-chat-concurrency1/profile_export.json' + NVIDIA GenAI-Perf | LLM Metrics +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┓ +┃ Statistic ┃ avg ┃ min ┃ max ┃ p99 ┃ p90 ┃ p75 ┃ +┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━┩ +│ Time to first token (ms) │ 71.66 │ 64.32 │ 86.52 │ 76.13 │ 74.92 │ 73.26 │ +│ Inter token latency (ms) │ 18.47 │ 18.25 │ 18.72 │ 18.67 │ 18.61 │ 18.53 │ +│ Request latency (ms) │ 348.00 │ 274.60 │ 362.27 │ 355.41 │ 352.29 │ 350.66 │ +│ Output sequence length │ 15.96 │ 12.00 │ 16.00 │ 16.00 │ 16.00 │ 16.00 │ +│ Input sequence length │ 549.66 │ 548.00 │ 551.00 │ 550.00 │ 550.00 │ 550.00 │ +│ Output token throughput (per sec) │ 45.84 │ N/A │ N/A │ N/A │ N/A │ N/A │ +│ Request throughput (per sec) │ 2.87 │ N/A │ N/A │ N/A │ N/A │ N/A │ +└───────────────────────────────────┴────────┴────────┴────────┴────────┴────────┴────────┘ +2024-10-14 22:44 [INFO] genai_perf.export_data.json_exporter:62 - Generating artifacts/llama-3.1-8b-instruct-openai-chat-concurrency1/profile_export_genai_perf.json +2024-10-14 22:44 [INFO] genai_perf.export_data.csv_exporter:71 - Generating artifacts/llama-3.1-8b-instruct-openai-chat-concurrency1/profile_export_genai_perf.csv +``` + +
+ +6. Use the OpenAI python client directly: +```python +from openai import OpenAI + +client = OpenAI( + base_url="http://localhost:9000/v1", + api_key="EMPTY", +) + +model = "llama-3.1-8b-instruct" +completion = client.chat.completions.create( + model=model, + messages=[ + { + "role": "system", + "content": "You are a helpful assistant.", + }, + {"role": "user", "content": "What are LLMs?"}, + ], + max_tokens=256, +) + +print(completion.choices[0].message.content) +``` + +7. Run tests (NOTE: The server should not be running, the tests will handle starting/stopping the server as necessary): +```bash +cd /opt/tritonserver/python/openai/ +pip install -r requirements-test.txt + +pytest -v tests/ +``` + +## TensorRT-LLM + +0. Prepare your model repository for a TensorRT-LLM model, build the engine, etc. You can try any of the following options: + - [Triton CLI](https://github.com/triton-inference-server/triton_cli/) + - [TRT-LLM Backend Quickstart](https://github.com/triton-inference-server/tensorrtllm_backend?tab=readme-ov-file#quick-start) + +1. Launch the container: + - Mounts the `~/.huggingface/cache` for re-use of downloaded models across runs, containers, etc. + - Sets the [`HF_TOKEN`](https://huggingface.co/docs/huggingface_hub/en/package_reference/environment_variables#hftoken) environment variable to + access gated models, make sure this is set in your local environment if needed. + +```bash +docker run -it --net=host --gpus all --rm \ + -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ + -e HF_TOKEN \ + -e TRTLLM_ORCHESTRATOR=1 \ + nvcr.io/nvidia/tritonserver:24.11-trtllm-python-py3 +``` + +2. Install dependencies inside the container: +```bash +# Install python bindings for tritonserver and tritonfrontend +pip install /opt/tritonserver/python/triton*.whl + +# Install application requirements +git clone https://github.com/triton-inference-server/server.git +cd server/python/openai/ +pip install -r requirements.txt +``` + +2. Launch the OpenAI server: +```bash +# NOTE: Adjust the --tokenizer based on the model being used +python3 openai_frontend/main.py --model-repository path/to/models --tokenizer meta-llama/Meta-Llama-3.1-8B-Instruct +``` + +3. Send a `/v1/chat/completions` request: + - Note the use of `jq` is optional, but provides a nicely formatted output for JSON responses. +```bash +# MODEL should be the client-facing model name in your model repository for a pipeline like TRT-LLM. +# For example, this could also be "ensemble", or something like "gpt2" if generated from Triton CLI +MODEL="tensorrt_llm_bls" +curl -s http://localhost:9000/v1/chat/completions -H 'Content-Type: application/json' -d '{ + "model": "'${MODEL}'", + "messages": [{"role": "user", "content": "Say this is a test!"}] +}' | jq +``` + +
+Example output + +```json +{ + "id": "cmpl-704c758c-8a84-11ef-b106-107c6149ca79", + "choices": [ + { + "finish_reason": "stop", + "index": 0, + "message": { + "content": "It looks like you're testing the system!", + "tool_calls": null, + "role": "assistant", + "function_call": null + }, + "logprobs": null + } + ], + "created": 1728948689, + "model": "llama-3-8b-instruct", + "system_fingerprint": null, + "object": "chat.completion", + "usage": null +} +``` + +
+ +The other examples should be the same as vLLM, except that you should set `MODEL="tensorrt_llm_bls"` or `MODEL="ensemble"`, +everywhere applicable as seen in the example request above. + +## KServe Frontends + +To support serving requests through both the OpenAI-Compatible and +KServe Predict v2 frontends to the same running Triton Inference Server, +the `tritonfrontend` python bindings are included for optional use in this +application as well. + +You can opt-in to including these additional frontends, assuming `tritonfrontend` +is installed, with `--enable-kserve-frontends` like below: + +``` +python3 openai_frontend/main.py \ + --model-repository tests/vllm_models \ + --tokenizer meta-llama/Meta-Llama-3.1-8B-Instruct \ + --enable-kserve-frontends +``` + +See `python3 openai_frontend/main.py --help` for more information on the +available arguments and default values. + +For more information on the `tritonfrontend` python bindings, see the docs +[here](https://github.com/triton-inference-server/server/blob/main/docs/customization_guide/tritonfrontend.md). + +## Model Parallelism Support + +- [x] vLLM ([EngineArgs](https://github.com/triton-inference-server/vllm_backend/blob/main/README.md#using-the-vllm-backend)) + - ex: Configure `tensor_parallel_size: 2` in the + [model.json](https://github.com/triton-inference-server/vllm_backend/blob/main/samples/model_repository/vllm_model/1/model.json) +- [x] TensorRT-LLM ([Orchestrator Mode](https://github.com/triton-inference-server/tensorrtllm_backend/blob/main/README.md#orchestrator-mode)) + - Set the following environment variable: `export TRTLLM_ORCHESTRATOR=1` +- [ ] TensorRT-LLM ([Leader Mode](https://github.com/triton-inference-server/tensorrtllm_backend/blob/main/README.md#leader-mode)) + - Not currently supported diff --git a/docs/client_guide/python.rst b/docs/client_guide/python.rst index e546f7b18e..545f4f6042 100644 --- a/docs/client_guide/python.rst +++ b/docs/client_guide/python.rst @@ -1,5 +1,5 @@ .. -.. Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. .. Redistribution and use in source and binary forms, with or without .. modification, are permitted provided that the following conditions @@ -29,10 +29,11 @@ Python #### +.. include:: python_readme.rst + .. toctree:: :maxdepth: 1 :hidden: - Overview <../tutorials/Triton_Inference_Server_Python_API/README.md> Kafka I/O <../tutorials/Triton_Inference_Server_Python_API/examples/kafka-io/README.md> Rayserve <../tutorials/Triton_Inference_Server_Python_API/examples/rayserve/README.md> \ No newline at end of file diff --git a/docs/client_guide/python_readme.rst b/docs/client_guide/python_readme.rst new file mode 100644 index 0000000000..e7a79abe60 --- /dev/null +++ b/docs/client_guide/python_readme.rst @@ -0,0 +1,268 @@ +.. +.. Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. +.. Redistribution and use in source and binary forms, with or without +.. modification, are permitted provided that the following conditions +.. are met: +.. * Redistributions of source code must retain the above copyright +.. notice, this list of conditions and the following disclaimer. +.. * Redistributions in binary form must reproduce the above copyright +.. notice, this list of conditions and the following disclaimer in the +.. documentation and/or other materials provided with the distribution. +.. * Neither the name of NVIDIA CORPORATION nor the names of its +.. contributors may be used to endorse or promote products derived +.. from this software without specific prior written permission. +.. +.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +.. EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +.. PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +.. CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +.. EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +.. PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +.. PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +.. OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +.. (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +.. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +.. raw:: html + + +Triton Inference Server In-Process Python API [BETA] +==================================================== + +Starting with release 24.01 Triton Inference Server will include a +Python package enabling developers to embed Triton Inference Server +instances in their Python applications. The in-process Python API is +designed to match the functionality of the in-process C API while +providing a higher level abstraction. At its core the API relies on a +1:1 python binding of the C API and provides all the flexibility and +power of the C API with a simpler to use interface. + + [!Note] As the API is in BETA please expect some changes as we test + out different features and get feedback. All feedback is weclome and + we look forward to hearing from you! + +| `Requirements <#requirements>`__ \| `Installation <#installation>`__ + \| `Hello World <#hello-world>`__ \| `Stable + Diffusion <#stable-diffusion>`__ \| `Ray Serve + Deployment <../tutorials/Triton_Inference_Server_Python_API/examples/rayserve>`__ \| +Requirements +------------ + +The following instructions require a linux system with Docker installed. +For CUDA support, make sure your CUDA driver meets the requirements in +“NVIDIA Driver” section of Deep Learning Framework support matrix: +https://docs.nvidia.com/deeplearning/frameworks/support-matrix/index.html + +Installation +------------ + +The tutorial and Python API package are designed to be installed and run +within the ``nvcr.io/nvidia/tritonserver:24.01-py3`` docker image. + +A set of convenience scripts are provided to create a docker image based +on the ``nvcr.io/nvidia/tritonserver:24.01-py3`` image with the Python +API installed plus additional dependencies required for the examples. + +Triton Inference Server 24.01 + Python API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Clone Repository +^^^^^^^^^^^^^^^^ + +.. code:: bash + git clone https://github.com/triton-inference-server/tutorials.git + cd tutorials/Triton_Inference_Server_Python_API +Build ``triton-python-api:r24.01`` Image +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: bash + ./build.sh +Supported Backends +^^^^^^^^^^^^^^^^^^ + +The built image includes all the backends shipped by default in the +tritonserver ``nvcr.io/nvidia/tritonserver:24.01-py3`` container. + +:: + + dali fil identity onnxruntime openvino python pytorch repeat square tensorflow tensorrt + +Included Models +^^^^^^^^^^^^^^^ + +The ``default`` build includes an ``identity`` model that can be used +for exercising basic operations including sending input tensors of +different data types. The ``identity`` model copies provided inputs of +``shape [-1, -1]`` to outputs of shape ``[-1, -1]``. Inputs are named +``data_type_input`` and outputs are named ``data_type_output`` +(e.g. ``string_input``, ``string_output``, ``fp16_input``, +``fp16_output``). + +Hello World +----------- + +Start ``triton-python-api:r24.01`` Container +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following command starts a container and volume mounts the current +directory as ``workspace``. + +.. code:: bash + ./run.sh +Enter Python Shell +~~~~~~~~~~~~~~~~~~ + +.. code:: bash + python3 +Create and Start a Server Instance +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + import tritonserver + server = tritonserver.Server(model_repository="/workspace/identity-models") + server.start() +List Models +~~~~~~~~~~~ + +:: + + server.models() + +Example Output +^^^^^^^^^^^^^^ + +``server.models()`` returns a dictionary of the available models with +their current state. + +.. code:: python + {('identity', 1): {'name': 'identity', 'version': 1, 'state': 'READY'}} +Send an Inference Request +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + model = server.model("identity") + responses = model.infer(inputs={"string_input":[["hello world!"]]}) +Iterate through Responses +~~~~~~~~~~~~~~~~~~~~~~~~~ + +``model.infer()`` returns an iterator that can be used to process the +results of an inference request. + +.. code:: python + for response in responses: + print(response.outputs["string_output"].to_string_array()) +.. _example-output-1: + +Example Output +^^^^^^^^^^^^^^ + +.. code:: python + [['hello world!']] +Stable Diffusion +---------------- + +This example is based on the +`Popular_Models_Guide/StableDiffusion <../tutorials/Popular_Models_Guide/StableDiffusion/README.html>`__ +tutorial. + +Build ``triton-python-api:r24.01-diffusion`` Image and Stable Diffusion Models +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Please note the following command will take many minutes depending on +your hardware configuration and network connection. + +.. code:: bash + ./build.sh --framework diffusion --build-models +.. _supported-backends-1: + +Supported Backends +^^^^^^^^^^^^^^^^^^ + +The built image includes all the backends shipped by default in the +tritonserver ``nvcr.io/nvidia/tritonserver:24.01-py3`` container. + +:: + + dali fil identity onnxruntime openvino python pytorch repeat square tensorflow tensorrt + +.. _included-models-1: + +Included Models +^^^^^^^^^^^^^^^ + +The ``diffusion`` build includes a ``stable_diffustion`` pipeline that +takes a text prompt and returns a generated image. For more details on +the models and pipeline please see the +`Popular_Models_Guide/StableDiffusion <../tutorials/Popular_Models_Guide/StableDiffusion/README.html>`__ +tutorial. + +Start Container +~~~~~~~~~~~~~~~ + +The following command starts a container and volume mounts the current +directory as ``workspace``. + +.. code:: bash + ./run.sh --framework diffusion +.. _enter-python-shell-1: + +Enter Python Shell +~~~~~~~~~~~~~~~~~~ + +.. code:: bash + python3 +.. _create-and-start-a-server-instance-1: + +Create and Start a Server Instance +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + import tritonserver + import numpy + from PIL import Image + server = tritonserver.Server(model_repository="/workspace/diffusion-models") + server.start() +.. _list-models-1: + +List Models +~~~~~~~~~~~ + +:: + + server.models() + +.. _example-output-2: + +Example Output +^^^^^^^^^^^^^^ + +.. code:: python + {('stable_diffusion', 1): {'name': 'stable_diffusion', 'version': 1, 'state': 'READY'}, ('text_encoder', 1): {'name': 'text_encoder', 'version': 1, 'state': 'READY'}, ('vae', 1): {'name': 'vae', 'version': 1, 'state': 'READY'}} +.. _send-an-inference-request-1: + +Send an Inference Request +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + model = server.model("stable_diffusion") + responses = model.infer(inputs={"prompt":[["butterfly in new york, realistic, 4k, photograph"]]}) +Iterate through Responses and save image +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + for response in responses: + generated_image = numpy.from_dlpack(response.outputs["generated_image"]) + generated_image = generated_image.squeeze().astype(numpy.uint8) + image_ = Image.fromarray(generated_image) + image_.save("sample_generated_image.jpg") +.. _example-output-3: + +Example Output +^^^^^^^^^^^^^^ + +.. figure:: ../tutorials/Triton_Inference_Server_Python_API/docs/sample_generated_image.jpg + :alt: sample_generated_image + + sample_generated_image \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 387e0f3c37..0b44f7c8b2 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -35,12 +35,9 @@ # -- Path setup -------------------------------------------------------------- import json -import logging import os import re -import subprocess from datetime import date -from logging.handlers import RotatingFileHandler # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -64,45 +61,6 @@ # at the end of the file. # current_dir = os.getcwd() # os.chdir("docs") -# -- Setup logger ------------------------------------------------------------ - - -def setup_logger(name, log_file, level=logging.INFO, max_bytes=1048576, backup_count=5): - logger = logging.getLogger(name) - logger.setLevel(level) - - # Prevent adding multiple handlers if the function is called multiple times - if not logger.handlers: - # Create handlers - file_handler = RotatingFileHandler( - log_file, maxBytes=max_bytes, backupCount=backup_count - ) - console_handler = logging.StreamHandler() - - # Set the logging level for handlers - file_handler.setLevel(level) - console_handler.setLevel(level) - - # Create a logging format - BLUE = "\033[94m" - RESET = "\033[0m" - formatter = logging.Formatter( - f"{BLUE}%(asctime)s - %(name)s - %(levelname)s - {RESET}%(message)s" - ) - file_handler.setFormatter(formatter) - console_handler.setFormatter(formatter) - - # Add handlers to the logger - logger.addHandler(file_handler) - logger.addHandler(console_handler) - return logger - - -logger = setup_logger( - os.path.basename(__file__), - os.environ.get("TRITON_SERVER_DOCS_LOG_FILE", "/tmp/docs.log"), -) -logger.info(f"Defined logger for {os.path.basename(__file__)}") # -- Project information ----------------------------------------------------- @@ -112,19 +70,14 @@ def setup_logger(name, log_file, level=logging.INFO, max_bytes=1048576, backup_c # Get the version of Triton this is building. version_long = "0.0.0" -logger.info(f"Getting version from ../TRITON_VERSION") with open("../TRITON_VERSION") as f: version_long = f.readline() version_long = version_long.strip() - logger.info(f"Version: {version_long}") - version_short = re.match(r"^[\d]+\.[\d]+\.[\d]+", version_long).group(0) -logger.info(f"Version short: {version_short}") version_short_split = version_short.split(".") -logger.info(f"Version short split: {version_short_split}") one_before = f"{version_short_split[0]}.{int(version_short_split[1]) - 1}.{version_short_split[2]}" -logger.info(f"One before: {one_before}") + # maintain left-side bar toctrees in `contents` file # so it doesn't show up needlessly in the index page @@ -227,7 +180,7 @@ def setup_logger(name, log_file, level=logging.INFO, max_bytes=1048576, backup_c "json_url": "https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/_static/switcher.json", "version_match": one_before if "dev" in version_long else version_short, }, - # "navbar_start": ["navbar-logo", "version-switcher"], + "navbar_start": ["navbar-logo", "version-switcher"], "primary_sidebar_end": [], } @@ -241,8 +194,6 @@ def setup_logger(name, log_file, level=logging.INFO, max_bytes=1048576, backup_c } ) -logger.info(f"html_theme_options: {html_theme_options}") - deploy_ngc_org = "nvidia" deploy_ngc_team = "triton" myst_substitutions = { @@ -252,8 +203,6 @@ def setup_logger(name, log_file, level=logging.INFO, max_bytes=1048576, backup_c else deploy_ngc_org, } -logger.info(f"myst_substitutions: {myst_substitutions}") - def ultimateReplace(app, docname, source): result = source[0] @@ -270,7 +219,6 @@ def ultimateReplace(app, docname, source): if deploy_ngc_team else deploy_ngc_org, } -logger.info(f"ultimate_replacements: {ultimate_replacements}") # bibtex_bibfiles = ["references.bib"] # To test that style looks good with common bibtex config @@ -285,24 +233,25 @@ def ultimateReplace(app, docname, source): # SETUP SWITCHER ############################### switcher_path = os.path.join(html_static_path[0], "switcher.json") -logger.info(f"switcher_path: {switcher_path}") versions = [] +# Triton 2 releases +correction = -1 if "dev" in version_long else 0 +upper_bound = version_short.split(".")[1] +for i in range(2, int(version_short.split(".")[1]) + correction): + versions.append((f"2.{i}.0", f"triton-inference-server-2{i}0")) -# Obtain Triton Server Release Tags. -tags = subprocess.run(["git", "tag", "--list", "v*"], capture_output=True, text=True) -tags_list = sorted(tags.stdout.strip().splitlines(), key=Version, reverse=True) -logger.info(f"Found source tags: {tags_list}") - -for v in tags_list: - versions.append( - ( - v.replace("v", ""), - f"triton-inference-server-{v.replace('v', '').replace('.', '')}", - ) - ) +# Triton 1 releases +for i in range(0, 15): + versions.append((f"1.{i}.0", f"tensorrt_inference_server_1{i}0")) + +# Triton Beta Releases +for i in range(1, 11): + versions.append((f"0.{i}.0_beta", f"inference_server_0{i}0_beta")) -logger.info(f"Defined dictionary of versions: {versions}") +# Patch releases +# Add here. +versions = sorted(versions, key=lambda v: Version(v[0]), reverse=True) # Build switcher data json_data = [] @@ -314,7 +263,6 @@ def ultimateReplace(app, docname, source): "url": f"https://docs.nvidia.com/deeplearning/triton-inference-server/archives/{v[1]}/user-guide/docs", } ) - if "dev" in version_long: json_data.insert( 0, @@ -336,7 +284,6 @@ def ultimateReplace(app, docname, source): # Trim to last N releases. json_data = json_data[0:12] -logger.info(f"Trimmed to last 12 release...") json_data.append( { @@ -346,22 +293,19 @@ def ultimateReplace(app, docname, source): } ) +# validate the links for i, d in enumerate(json_data): - logger.info(f"Validating link: {d['url']}") h = httplib2.Http() resp = h.request(d["url"], "HEAD") if int(resp[0]["status"]) >= 400: print(d["url"], "NOK", resp[0]["status"]) - # exit(1) + exit(1) -logger.info(f"Writing switcher data to file: {switcher_path}") +# Write switcher data to file with open(switcher_path, "w") as f: json.dump(json_data, f, ensure_ascii=False, indent=4) -logger.info("Configuration completed...") - - def setup(app): app.add_config_value("ultimate_replacements", {}, True) app.connect("source-read", ultimateReplace) diff --git a/docs/contents.rst b/docs/contents.rst index 420293aa76..dfff933e31 100644 --- a/docs/contents.rst +++ b/docs/contents.rst @@ -1,5 +1,5 @@ .. -.. Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. .. Redistribution and use in source and binary forms, with or without .. modification, are permitted provided that the following conditions @@ -36,8 +36,8 @@ :hidden: :caption: Getting Started - getting_started/quick_deployment - LLM With TensorRT-LLM + getting_started/quick_deployment_by_backend + LLM With TRT-LLM Multimodal model <../tutorials/Popular_Models_Guide/Llava1.5/llava_trtllm_guide.md> Stable diffusion <../tutorials/Popular_Models_Guide/StableDiffusion/README.md> @@ -54,7 +54,7 @@ Constrained Decoding <../tutorials/Feature_Guide/Constrained_Decoding/README.md> Function Calling <../tutorials/Feature_Guide/Function_Calling/README.md> - llm_features/speculative_decoding + llm_features/speculative_decoding_by_backend_type .. toctree:: :hidden: @@ -96,11 +96,12 @@ :hidden: :caption: Backends - TensorRT-LLM + TRT-LLM vLLM Python - PyTorch + Pytorch ONNX Runtime + TensorFlow TensorRT FIL DALI @@ -108,7 +109,7 @@ .. toctree:: :hidden: - :caption: Performance benchmarking and tuning + :caption: Perf benchmarking and tuning GenAI Perf Analyzer Performance Analyzer diff --git a/docs/customization_guide/build.md b/docs/customization_guide/build.md index 48495c7211..478f804bd4 100644 --- a/docs/customization_guide/build.md +++ b/docs/customization_guide/build.md @@ -1,5 +1,5 @@ - export RELEASE="26.05" + export RELEASE="24.07" docker run -it --net=host --gpus '"device=0"' nvcr.io/nvidia/tritonserver:${RELEASE}-py3-sdk 17. ## Download the Phi-3 tokenizer @@ -354,7 +354,7 @@ All config files inside /tensorrtllm\_backend/all\_models/inflight\_batcher\_llm
ensemble/config.pbtxt - # Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -864,7 +864,7 @@ All config files inside /tensorrtllm\_backend/all\_models/inflight\_batcher\_llm
postprocessing/config.pbtxt - # Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -993,7 +993,7 @@ All config files inside /tensorrtllm\_backend/all\_models/inflight\_batcher\_llm
preprocessing/config.pbtxt - # Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -1188,7 +1188,7 @@ All config files inside /tensorrtllm\_backend/all\_models/inflight\_batcher\_llm tensorrt_llm/config.pbtxt - # Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions diff --git a/docs/getting_started/quick_deployment.rst b/docs/getting_started/quick_deployment_by_backend.rst similarity index 92% rename from docs/getting_started/quick_deployment.rst rename to docs/getting_started/quick_deployment_by_backend.rst index b20775684a..aefa56787b 100644 --- a/docs/getting_started/quick_deployment.rst +++ b/docs/getting_started/quick_deployment_by_backend.rst @@ -1,5 +1,5 @@ .. -.. Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. .. Redistribution and use in source and binary forms, with or without .. modification, are permitted provided that the following conditions @@ -29,14 +29,16 @@ Quick Deployment Guide by backend #### +.. include:: quick_start.rst + .. toctree:: :maxdepth: 1 :hidden: - Quickstart TRT-LLM vLLM <../tutorials/Popular_Models_Guide/Llama2/vllm_guide.md> Python with HuggingFace <../tutorials/Quick_Deploy/HuggingFaceTransformers/README.md> PyTorch <../tutorials/Quick_Deploy/PyTorch/README.md> ONNX <../tutorials/Quick_Deploy/ONNX/README.md> + TensorFlow <../tutorials/Quick_Deploy/TensorFlow/README.md> Openvino <../tutorials/Quick_Deploy/OpenVINO/README.md> \ No newline at end of file diff --git a/docs/getting_started/quick_start.rst b/docs/getting_started/quick_start.rst new file mode 100644 index 0000000000..27f100e3cd --- /dev/null +++ b/docs/getting_started/quick_start.rst @@ -0,0 +1,175 @@ +.. +.. Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. +.. Redistribution and use in source and binary forms, with or without +.. modification, are permitted provided that the following conditions +.. are met: +.. * Redistributions of source code must retain the above copyright +.. notice, this list of conditions and the following disclaimer. +.. * Redistributions in binary form must reproduce the above copyright +.. notice, this list of conditions and the following disclaimer in the +.. documentation and/or other materials provided with the distribution. +.. * Neither the name of NVIDIA CORPORATION nor the names of its +.. contributors may be used to endorse or promote products derived +.. from this software without specific prior written permission. +.. +.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +.. EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +.. PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +.. CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +.. EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +.. PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +.. PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +.. OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +.. (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +.. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +.. raw:: html + + +Quickstart +========== + +**New to Triton Inference Server and want do just deploy your model +quickly?** Make use of `these +tutorials <../tutorials/README.html#quick-deploy>`__ to begin your Triton +journey! + +The Triton Inference Server is available as `buildable source +code <../customization_guide/build.html>`__, but the easiest way to +install and run Triton is to use the pre-built Docker image available +from the `NVIDIA GPU Cloud (NGC) `__. + +Launching and maintaining Triton Inference Server revolves around the +use of building model repositories. This tutorial will cover: + +- Creating a Model Repository +- Launching Triton +- Send an Inference Request + +Create A Model Repository +------------------------- + +The `model repository <../user_guide/model_repository.html>`__ is the +directory where you place the models that you want Triton to serve. An +example model repository is included in the +`docs/examples/model_repository `__. +Before using the repository, you must fetch any missing model definition +files from their public model zoos via the provided script. + +:: + + $ cd docs/examples + $ ./fetch_models.sh + +Launch Triton +------------- + +Triton is optimized to provide the best inferencing performance by using +GPUs, but it can also work on CPU-only systems. In both cases you can +use the same Triton Docker image. + +Run on System with GPUs +~~~~~~~~~~~~~~~~~~~~~~~ + +Use the following command to run Triton with the example model +repository you just created. The `NVIDIA Container +Toolkit `__ must be installed +for Docker to recognize the GPU(s). The –gpus=1 flag indicates that 1 +system GPU should be made available to Triton for inferencing. + +:: + + $ docker run --gpus=1 --rm -p8000:8000 -p8001:8001 -p8002:8002 -v/full/path/to/docs/examples/model_repository:/models nvcr.io/nvidia/tritonserver:-py3 tritonserver --model-repository=/models + +Where is the version of Triton that you want to use (and pulled +above). After you start Triton you will see output on the console +showing the server starting up and loading the model. When you see +output like the following, Triton is ready to accept inference requests. + +:: + + +----------------------+---------+--------+ + | Model | Version | Status | + +----------------------+---------+--------+ + | | | READY | + | .. | . | .. | + | .. | . | .. | + +----------------------+---------+--------+ + ... + ... + ... + I1002 21:58:57.891440 62 grpc_server.cc:3914] Started GRPCInferenceService at 0.0.0.0:8001 + I1002 21:58:57.893177 62 http_server.cc:2717] Started HTTPService at 0.0.0.0:8000 + I1002 21:58:57.935518 62 http_server.cc:2736] Started Metrics Service at 0.0.0.0:8002 + +All the models should show “READY” status to indicate that they loaded +correctly. If a model fails to load the status will report the failure +and a reason for the failure. If your model is not displayed in the +table check the path to the model repository and your CUDA drivers. + +Run on CPU-Only System +~~~~~~~~~~~~~~~~~~~~~~ + +On a system without GPUs, Triton should be run without using the –gpus +flag to Docker, but is otherwise identical to what is described above. + +:: + + $ docker run --rm -p8000:8000 -p8001:8001 -p8002:8002 -v/full/path/to/docs/examples/model_repository:/models nvcr.io/nvidia/tritonserver:-py3 tritonserver --model-repository=/models + +Because the –gpus flag is not used, a GPU is not available and Triton +will therefore be unable to load any model configuration that requires a +GPU. + +Verify Triton Is Running Correctly +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Use Triton’s *ready* endpoint to verify that the server and the models +are ready for inference. From the host system use curl to access the +HTTP endpoint that indicates server status. + +:: + + $ curl -v localhost:8000/v2/health/ready + ... + < HTTP/1.1 200 OK + < Content-Length: 0 + < Content-Type: text/plain + +The HTTP request returns status 200 if Triton is ready and non-200 if it +is not ready. + +Send an Inference Request +------------------------- + +Use docker pull to get the client libraries and examples image from NGC. + +:: + + $ docker pull nvcr.io/nvidia/tritonserver:-py3-sdk + +Where is the version that you want to pull. Run the client +image. + +:: + + $ docker run -it --rm --net=host nvcr.io/nvidia/tritonserver:-py3-sdk + +From within the nvcr.io/nvidia/tritonserver:-py3-sdk image, run +the example image-client application to perform image classification +using the example densenet_onnx model. + +To send a request for the densenet_onnx model use an image from the +/workspace/images directory. In this case we ask for the top 3 +classifications. + +:: + + $ /workspace/install/bin/image_client -m densenet_onnx -c 3 -s INCEPTION /workspace/images/mug.jpg + Request 0, batch size 1 + Image '/workspace/images/mug.jpg': + 15.346230 (504) = COFFEE MUG + 13.224326 (968) = CUP + 10.422965 (505) = COFFEEPOT \ No newline at end of file diff --git a/docs/getting_started/trtllm_user_guide.md b/docs/getting_started/trtllm_user_guide.md index a47d0c471d..7f128e98c7 100644 --- a/docs/getting_started/trtllm_user_guide.md +++ b/docs/getting_started/trtllm_user_guide.md @@ -1,5 +1,5 @@ -# [Triton Inference Server Release 26.05](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-26-05.html#rel-26-05) +# [Triton Inference Server Release 25.03](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-25-03.html#rel-25-03) -The Triton Inference Server container image, release 26.05, is available +The Triton Inference Server container image, release 25.03, is available on [NGC](https://ngc.nvidia.com/catalog/containers/nvidia:tritonserver) and is open source on [GitHub](https://github.com/triton-inference-server/server). Release notes can -be found on the [GitHub Release Page](https://github.com/triton-inference-server/server/releases) +be found on the [GitHub Release Page](https://github.com/triton-inference-server/server/releases) \ No newline at end of file diff --git a/docs/llm_features/speculative_decoding.rst b/docs/llm_features/speculative_decoding.rst index 7e0df42bc5..debbcf52ae 100644 --- a/docs/llm_features/speculative_decoding.rst +++ b/docs/llm_features/speculative_decoding.rst @@ -1,5 +1,5 @@ .. -.. Copyright 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. .. Redistribution and use in source and binary forms, with or without .. modification, are permitted provided that the following conditions @@ -25,14 +25,30 @@ .. (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE .. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#### -Speculative Decoding -#### +.. raw:: html -.. toctree:: - :maxdepth: 1 - :hidden: - Overview <../tutorials/Feature_Guide/Speculative_Decoding/README.md> - TRT-LLM <../tutorials/Feature_Guide/Speculative_Decoding/TRT-LLM/README.md> - vLLM <../tutorials/Feature_Guide/Speculative_Decoding/vLLM/README.md> \ No newline at end of file +About Speculative Decoding +========================= +Speculative Decoding (also referred to as Speculative Sampling) is a set of techniques designed +to allow generation of more than one token per forward pass iteration. This can lead to a reduction +in the average per-token latency in situations where the GPU is underutilized due to small batch sizes. + +Speculative decoding involves predicting a sequence of future tokens, referred to as draft tokens, +using a method that is substantially more efficient than repeatedly executing the target Large Language +Model (LLM). These draft tokens are then collectively validated by processing them through the target LLM +in a single forward pass. The underlying assumptions are twofold: + +1. processing multiple draft tokens concurrently will be as rapid as processing a single token +2. multiple draft tokens will be validated successfully over the course of the full generation + +If the first assumption holds true, the latency of speculative decoding will no worse than the standard +approach. If the second holds, output token generation advances by statistically more than one token per +forward pass. The combination of both these allows speculative decoding to result in reduced latency. + +Performance Improvements +======================== +It's important to note that the effectiveness of speculative decoding techniques is highly dependent +on the specific task at hand. For instance, forecasting subsequent tokens in a code-completion scenario +may prove simpler than generating a summary for an article. `Spec-Bench `__ +shows the performance of different speculative decoding approaches on different tasks. \ No newline at end of file diff --git a/docs/llm_features/speculative_decoding_by_backend_type.rst b/docs/llm_features/speculative_decoding_by_backend_type.rst new file mode 100644 index 0000000000..a61f625626 --- /dev/null +++ b/docs/llm_features/speculative_decoding_by_backend_type.rst @@ -0,0 +1,39 @@ +.. +.. Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. +.. Redistribution and use in source and binary forms, with or without +.. modification, are permitted provided that the following conditions +.. are met: +.. * Redistributions of source code must retain the above copyright +.. notice, this list of conditions and the following disclaimer. +.. * Redistributions in binary form must reproduce the above copyright +.. notice, this list of conditions and the following disclaimer in the +.. documentation and/or other materials provided with the distribution. +.. * Neither the name of NVIDIA CORPORATION nor the names of its +.. contributors may be used to endorse or promote products derived +.. from this software without specific prior written permission. +.. +.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +.. EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +.. PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +.. CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +.. EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +.. PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +.. PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +.. OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +.. (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +.. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#### +Speculative Decoding +#### + +.. include:: speculative_decoding.rst + +.. toctree:: + :maxdepth: 1 + :hidden: + + TRT-LLM <../tutorials/Feature_Guide/Speculative_Decoding/TRT-LLM/README.md> + vLLM <../tutorials/Feature_Guide/Speculative_Decoding/vLLM/README.md> \ No newline at end of file diff --git a/docs/perf_benchmark/genai-perf-README.rst b/docs/perf_benchmark/genai-perf-README.rst new file mode 100644 index 0000000000..c4a3c7d73d --- /dev/null +++ b/docs/perf_benchmark/genai-perf-README.rst @@ -0,0 +1,686 @@ +.. +.. Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. +.. Redistribution and use in source and binary forms, with or without +.. modification, are permitted provided that the following conditions +.. are met: +.. * Redistributions of source code must retain the above copyright +.. notice, this list of conditions and the following disclaimer. +.. * Redistributions in binary form must reproduce the above copyright +.. notice, this list of conditions and the following disclaimer in the +.. documentation and/or other materials provided with the distribution. +.. * Neither the name of NVIDIA CORPORATION nor the names of its +.. contributors may be used to endorse or promote products derived +.. from this software without specific prior written permission. +.. +.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +.. EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +.. PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +.. CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +.. EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +.. PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +.. PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +.. OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +.. (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +.. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +.. raw:: html + + +GenAI-Perf +========== + +GenAI-Perf is a command line tool for measuring the throughput and +latency of generative AI models as served through an inference server. +For large language models (LLMs), GenAI-Perf provides metrics such as +`output token throughput <#output_token_throughput_metric>`__, `time to +first token <#time_to_first_token_metric>`__, `inter token +latency <#inter_token_latency_metric>`__, and `request +throughput <#request_throughput_metric>`__. For a full list of metrics +please see the `Metrics section <#metrics>`__. + +Users specify a model name, an inference server URL, the type of inputs +to use (synthetic or from dataset), and the type of load to generate +(number of concurrent requests, request rate). + +GenAI-Perf generates the specified load, measures the performance of the +inference server and reports the metrics in a simple table as console +output. The tool also logs all results in a csv and json file that can +be used to derive additional metrics and visualizations. The inference +server must already be running when GenAI-Perf is run. + +You can use GenAI-Perf to run performance benchmarks on - `Large +Language Models `__ - `Vision Language +Models `__ - `Embedding +Models `__ - `Ranking Models `__ - +`Multiple LoRA Adapters `__ + + [!Note] GenAI-Perf is currently in early release and under rapid + development. While we will try to remain consistent, command line + options and functionality are subject to change as the tool matures. + +.. raw:: html + + + +Installation +------------ + +The easiest way to install GenAI-Perf is through `Triton Server SDK +container `__. +Install the latest release using the following command: + +.. code:: bash + + export RELEASE="yy.mm" # e.g. export RELEASE="24.06" + + docker run -it --net=host --gpus=all nvcr.io/nvidia/tritonserver:${RELEASE}-py3-sdk + + # Check out genai_perf command inside the container: + genai-perf --help + +.. raw:: html + +
+ +Alternatively, to install from source: + +Since GenAI-Perf depends on Perf Analyzer, you’ll need to install the +Perf Analyzer binary: + +Install Perf Analyzer (Ubuntu, Python 3.8+) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**NOTE**: you must already have CUDA 12 installed (checkout the `CUDA +installation +guide `__). + +.. code:: bash + + pip install tritonclient + + apt update && apt install -y --no-install-recommends libb64-0d libcurl4 + +You can also build Perf Analyzer `from +source <../docs/install.md#build-from-source>`__ as well. + +Install GenAI-Perf from source +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: bash + + git clone https://github.com/triton-inference-server/perf_analyzer.git && cd perf_analyzer + + pip install -e genai-perf + +.. raw:: html + +
+ +.. raw:: html + + + +Quick Start +----------- + +In this quick start, we will use GenAI-Perf to run performance +benchmarking on the GPT-2 model running on Triton Inference Server with +a TensorRT-LLM engine. + +Serve GPT-2 TensorRT-LLM model using Triton CLI +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can follow the `quickstart +guide `__ +on Triton CLI github repo to run GPT-2 model locally. The full +instructions are copied below for convenience: + +.. code:: bash + + # This container comes with all of the dependencies for building TRT-LLM engines + # and serving the engine with Triton Inference Server. + docker run -ti \ + --gpus all \ + --network=host \ + --shm-size=1g --ulimit memlock=-1 \ + -v /tmp:/tmp \ + -v ${HOME}/models:/root/models \ + -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ + nvcr.io/nvidia/tritonserver:24.05-trtllm-python-py3 + + # Install the Triton CLI + pip install git+https://github.com/triton-inference-server/triton_cli.git@0.0.8 + + # Build TRT LLM engine and generate a Triton model repository pointing at it + triton remove -m all + triton import -m gpt2 --backend tensorrtllm + + # Start Triton pointing at the default model repository + triton start + +Running GenAI-Perf +~~~~~~~~~~~~~~~~~~ + +Now we can run GenAI-Perf from Triton Inference Server SDK container: + +.. code:: bash + + export RELEASE="yy.mm" # e.g. export RELEASE="24.06" + + docker run -it --net=host --rm --gpus=all nvcr.io/nvidia/tritonserver:${RELEASE}-py3-sdk + + # Run GenAI-Perf in the container: + genai-perf profile \ + -m gpt2 \ + --service-kind triton \ + --backend tensorrtllm \ + --num-prompts 100 \ + --random-seed 123 \ + --synthetic-input-tokens-mean 200 \ + --synthetic-input-tokens-stddev 0 \ + --streaming \ + --output-tokens-mean 100 \ + --output-tokens-stddev 0 \ + --output-tokens-mean-deterministic \ + --tokenizer hf-internal-testing/llama-tokenizer \ + --concurrency 1 \ + --measurement-interval 4000 \ + --profile-export-file my_profile_export.json \ + --url localhost:8001 + +Example output: + +:: + + LLM Metrics + ┏━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┓ + ┃ Statistic ┃ avg ┃ min ┃ max ┃ p99 ┃ p90 ┃ p75 ┃ + ┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━┩ + │ Time to first token (ms) │ 11.70 │ 9.88 │ 17.21 │ 14.35 │ 12.01 │ 11.87 │ + │ Inter token latency (ms) │ 1.46 │ 1.08 │ 1.89 │ 1.87 │ 1.62 │ 1.52 │ + │ Request latency (ms) │ 161.24 │ 153.45 │ 200.74 │ 200.66 │ 179.43 │ 162.23 │ + │ Output sequence length │ 103.39 │ 95.00 │ 134.00 │ 120.08 │ 107.30 │ 105.00 │ + │ Input sequence length │ 200.01 │ 200.00 │ 201.00 │ 200.13 │ 200.00 │ 200.00 │ + └──────────────────────────┴────────┴────────┴────────┴────────┴────────┴────────┘ + Output token throughput (per sec): 635.61 + Request throughput (per sec): 6.15 + +See `Tutorial `__ for additional examples. + +.. raw:: html + + + +Visualization +------------- + +GenAI-Perf can also generate various plots that visualize the +performance of the current profile run. This is disabled by default but +users can easily enable it by passing the ``--generate-plots`` option +when running the benchmark: + +.. code:: bash + + genai-perf profile \ + -m gpt2 \ + --service-kind triton \ + --backend tensorrtllm \ + --streaming \ + --concurrency 1 \ + --generate-plots + +This will generate a `set of default +plots `__ such as: - Time to first token +(TTFT) analysis - Request latency analysis - TTFT vs Input sequence +lengths - Inter token latencies vs Token positions - Input sequence +lengths vs Output sequence lengths + +Using ``compare`` Subcommand to Visualize Multiple Runs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``compare`` subcommand in GenAI-Perf facilitates users in comparing +multiple profile runs and visualizing the differences through plots. + +Usage +^^^^^ + +Assuming the user possesses two profile export JSON files, namely +``profile1.json`` and ``profile2.json``, they can execute the +``compare`` subcommand using the ``--files`` option: + +.. code:: bash + + genai-perf compare --files profile1.json profile2.json + +Executing the above command will perform the following actions under the +``compare`` directory: 1. Generate a YAML configuration file +(e.g. ``config.yaml``) containing the metadata for each plot generated +during the comparison process. 2. Automatically generate the `default +set of plots `__ (e.g. TTFT vs. Input +Sequence Lengths) that compare the two profile runs. + +:: + + compare + ├── config.yaml + ├── distribution_of_input_sequence_lengths_to_output_sequence_lengths.jpeg + ├── request_latency.jpeg + ├── time_to_first_token.jpeg + ├── time_to_first_token_vs_input_sequence_lengths.jpeg + ├── token-to-token_latency_vs_output_token_position.jpeg + └── ... + +Customization +^^^^^^^^^^^^^ + +Users have the flexibility to iteratively modify the generated YAML +configuration file to suit their specific requirements. They can make +alterations to the plots according to their preferences and execute the +command with the ``--config`` option followed by the path to the +modified configuration file: + +.. code:: bash + + genai-perf compare --config compare/config.yaml + +This command will regenerate the plots based on the updated +configuration settings, enabling users to refine the visual +representation of the comparison results as per their needs. + +See `Compare documentation `__ for more details. + +.. raw:: html + + + +Model Inputs +------------ + +GenAI-Perf supports model input prompts from either synthetically +generated inputs, or from the HuggingFace +`OpenOrca `__ or +`CNN_DailyMail `__ +datasets. This is specified using the ``--input-dataset`` CLI option. + +When the dataset is synthetic, you can specify the following options: \* +``--num-prompts ``: The number of unique prompts to generate as +stimulus, >= 1. \* ``--synthetic-input-tokens-mean ``: The mean of +number of tokens in the generated prompts when using synthetic data, >= +1. \* ``--synthetic-input-tokens-stddev ``: The standard deviation +of number of tokens in the generated prompts when using synthetic data, +>= 0. \* ``--random-seed ``: The seed used to generate random +values, >= 0. + +When the dataset is coming from HuggingFace, you can specify the +following options: \* ``--input-dataset {openorca,cnn_dailymail}``: +HuggingFace dataset to use for benchmarking. \* ``--num-prompts ``: +The number of unique prompts to generate as stimulus, >= 1. + +When the dataset is coming from a file, you can specify the following +options: \* ``--input-file ``: The input file containing the +prompts to use for benchmarking as JSON objects. + +For any dataset, you can specify the following options: \* +``--output-tokens-mean ``: The mean number of tokens in each +output. Ensure the ``--tokenizer`` value is set correctly, >= 1. \* +``--output-tokens-stddev ``: The standard deviation of the number +of tokens in each output. This is only used when output-tokens-mean is +provided, >= 1. \* ``--output-tokens-mean-deterministic``: When using +``--output-tokens-mean``, this flag can be set to improve precision by +setting the minimum number of tokens equal to the requested number of +tokens. This is currently supported with the Triton service-kind. Note +that there is still some variability in the requested number of output +tokens, but GenAi-Perf attempts its best effort with your model to get +the right number of output tokens. + +You can optionally set additional model inputs with the following +option: \* ``--extra-inputs :``: An additional input +for use with the model with a singular value, such as ``stream:true`` or +``max_tokens:5``. This flag can be repeated to supply multiple extra +inputs. + +For `Large Language Models `__, there is no batch size +(i.e. batch size is always ``1``). Each request includes the inputs for +one individual inference. Other modes such as the +`embeddings `__ and `rankings `__ +endpoints support client-side batching, where ``--batch-size N`` means +that each request sent will include the inputs for ``N`` separate +inferences, allowing them to be processed together. + +.. raw:: html + + + +Metrics +------- + +GenAI-Perf collects a diverse set of metrics that captures the +performance of the inference server. + ++-----------------------+-----------------------+-----------------------+ +| Metric | Description | Aggregations | ++=======================+=======================+=======================+ +| Time to First Token | Time between when a | Avg, min, max, p99, | +| | request is sent and | p90, p75 | +| | when its first | | +| | response is received, | | +| | one value per request | | +| | in benchmark | | ++-----------------------+-----------------------+-----------------------+ +| Inter Token Latency | Time between | Avg, min, max, p99, | +| | intermediate | p90, p75 | +| | responses for a | | +| | single request | | +| | divided by the number | | +| | of generated tokens | | +| | of the latter | | +| | response, one value | | +| | per response per | | +| | request in benchmark | | ++-----------------------+-----------------------+-----------------------+ +| Request Latency | Time between when a | Avg, min, max, p99, | +| | request is sent and | p90, p75 | +| | when its final | | +| | response is received, | | +| | one value per request | | +| | in benchmark | | ++-----------------------+-----------------------+-----------------------+ +| Output Sequence | Total number of | Avg, min, max, p99, | +| Length | output tokens of a | p90, p75 | +| | request, one value | | +| | per request in | | +| | benchmark | | ++-----------------------+-----------------------+-----------------------+ +| Input Sequence Length | Total number of input | Avg, min, max, p99, | +| | tokens of a request, | p90, p75 | +| | one value per request | | +| | in benchmark | | ++-----------------------+-----------------------+-----------------------+ +| Output Token | Total number of | None–one value per | +| Throughput | output tokens from | benchmark | +| | benchmark divided by | | +| | benchmark duration | | ++-----------------------+-----------------------+-----------------------+ +| Request Throughput | Number of final | None–one value per | +| | responses from | benchmark | +| | benchmark divided by | | +| | benchmark duration | | ++-----------------------+-----------------------+-----------------------+ + +.. raw:: html + + + +Command Line Options +-------------------- + +``-h`` +'''''' + +``--help`` +'''''''''' + +Show the help message and exit. + +Endpoint Options: +~~~~~~~~~~~~~~~~~ + +``-m `` +''''''''''''' + +``--model `` +'''''''''''''''''' + +The names of the models to benchmark. A single model is recommended, +unless you are `profiling multiple LoRA adapters `__. +(default: ``None``) + +``--model-selection-strategy {round_robin, random}`` +'''''''''''''''''''''''''''''''''''''''''''''''''''' + +When multiple models are specified, this is how a specific model is +assigned to a prompt. Round robin means that each model receives a +request in order. Random means that assignment is uniformly random +(default: ``round_robin``) + +``--backend {tensorrtllm,vllm}`` +'''''''''''''''''''''''''''''''' + +When using the “triton” service-kind, this is the backend of the model. +For the TRT-LLM backend, you currently must set +``exclude_input_in_output`` to true in the model config to not echo the +input tokens in the output. (default: tensorrtllm) + +``--endpoint `` +'''''''''''''''''''' + +Set a custom endpoint that differs from the OpenAI defaults. (default: +``None``) + +``--endpoint-type {chat,completions,embeddings,rankings}`` +'''''''''''''''''''''''''''''''''''''''''''''''''''''''''' + +The endpoint-type to send requests to on the server. This is only used +with the ``openai`` service-kind. (default: ``None``) + +``--service-kind {triton,openai}`` +'''''''''''''''''''''''''''''''''' + +The kind of service perf_analyzer will generate load for. In order to +use ``openai``, you must specify an api via ``--endpoint-type``. +(default: ``triton``) + +``--streaming`` +''''''''''''''' + +An option to enable the use of the streaming API. (default: ``False``) + +``-u `` +'''''''''''' + +``--url `` +''''''''''''''' + +URL of the endpoint to target for benchmarking. (default: ``None``) + +Input Options +~~~~~~~~~~~~~ + +``-b `` +'''''''''''' + +``--batch-size `` +'''''''''''''''''''''' + +The batch size of the requests GenAI-Perf should send. This is currently +only supported with the `embeddings `__, +image_retrieval, and `rankings `__ endpoint types. +(default: ``1``) + +``--extra-inputs `` +'''''''''''''''''''''''' + +Provide additional inputs to include with every request. You can repeat +this flag for multiple inputs. Inputs should be in an input_name:value +format. Alternatively, a string representing a json formatted dict can +be provided. (default: ``None``) + +``--input-dataset {openorca,cnn_dailymail}`` +'''''''''''''''''''''''''''''''''''''''''''' + +The HuggingFace dataset to use for prompts. (default: ``openorca``) + +``--input-file `` +''''''''''''''''''''''' + +The input file containing the prompts to use for profiling. Each line +should be a JSON object with a ‘text_input’ field in JSONL format. +Example: {"text_input": "Your prompt here"}" + +``--num-prompts `` +''''''''''''''''''''''' + +The number of unique prompts to generate as stimulus. (default: ``100``) + +``--output-tokens-mean `` +'''''''''''''''''''''''''''''' + +The mean number of tokens in each output. Ensure the ``--tokenizer`` +value is set correctly. (default: ``-1``) + +``--output-tokens-mean-deterministic`` +'''''''''''''''''''''''''''''''''''''' + +When using ``--output-tokens-mean``, this flag can be set to improve +precision by setting the minimum number of tokens equal to the requested +number of tokens. This is currently supported with the Triton +service-kind. Note that there is still some variability in the requested +number of output tokens, but GenAi-Perf attempts its best effort with +your model to get the right number of output tokens. (default: +``False``) + +``--output-tokens-stddev `` +'''''''''''''''''''''''''''''''' + +The standard deviation of the number of tokens in each output. This is +only used when ``--output-tokens-mean`` is provided. (default: ``0``) + +``--random-seed `` +''''''''''''''''''''''' + +The seed used to generate random values. (default: ``0``) + +``--synthetic-input-tokens-mean `` +''''''''''''''''''''''''''''''''''''''' + +The mean of number of tokens in the generated prompts when using +synthetic data. (default: ``550``) + +``--synthetic-input-tokens-stddev `` +''''''''''''''''''''''''''''''''''''''''' + +The standard deviation of number of tokens in the generated prompts when +using synthetic data. (default: ``0``) + +Profiling Options +~~~~~~~~~~~~~~~~~ + +``--concurrency `` +''''''''''''''''''''''' + +The concurrency value to benchmark. (default: ``None``) + +``--measurement-interval `` +'''''''''''''''''''''''''''''''' + +``-p `` +'''''''''''' + +The time interval used for each measurement in milliseconds. Perf +Analyzer will sample a time interval specified and take measurement over +the requests completed within that time interval. (default: ``10000``) + +``--request-rate `` +'''''''''''''''''''''''''' + +Sets the request rate for the load generated by PA. (default: ``None``) + +``-s `` +'''''''''''''' + +``--stability-percentage `` +'''''''''''''''''''''''''''''''''' + +The allowed variation in latency measurements when determining if a +result is stable. The measurement is considered as stable if the ratio +of max / min from the recent 3 measurements is within (stability +percentage) in terms of both infer per second and latency. (default: +``999``) + +Output Options +~~~~~~~~~~~~~~ + +``--artifact-dir`` +'''''''''''''''''' + +The directory to store all the (output) artifacts generated by +GenAI-Perf and Perf Analyzer. (default: ``artifacts``) + +``--generate-plots`` +'''''''''''''''''''' + +An option to enable the generation of plots. (default: False) + +``--profile-export-file `` +'''''''''''''''''''''''''''''''' + +The path where the perf_analyzer profile export will be generated. By +default, the profile export will be to ``profile_export.json``. The +genai-perf files will be exported to +``_genai_perf.json`` and +``_genai_perf.csv``. For example, if the profile +export file is ``profile_export.json``, the genai-perf file will be +exported to ``profile_export_genai_perf.csv``. (default: +``profile_export.json``) + +Other Options +~~~~~~~~~~~~~ + +``--tokenizer `` +''''''''''''''''''''' + +The HuggingFace tokenizer to use to interpret token metrics from prompts +and responses. (default: ``hf-internal-testing/llama-tokenizer``) + +``-v`` +'''''' + +``--verbose`` +''''''''''''' + +An option to enable verbose mode. (default: ``False``) + +``--version`` +''''''''''''' + +An option to print the version and exit. + +.. raw:: html + + + +Known Issues +------------ + +- GenAI-Perf can be slow to finish if a high request-rate is provided +- Token counts may not be exact diff --git a/docs/perf_benchmark/genai_perf.rst b/docs/perf_benchmark/genai_perf.rst index 65de2d8ce1..175662477f 100644 --- a/docs/perf_benchmark/genai_perf.rst +++ b/docs/perf_benchmark/genai_perf.rst @@ -1,5 +1,5 @@ .. -.. Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. .. Redistribution and use in source and binary forms, with or without .. modification, are permitted provided that the following conditions @@ -28,12 +28,13 @@ #### GenAI Performance Analyzer #### +.. include:: genai-perf-README.rst + .. toctree:: :maxdepth: 1 :hidden: - Overview <../perf_analyzer/genai-perf/README.md> Large language models <../perf_analyzer/genai-perf/docs/tutorial.md> Visual language models <../perf_analyzer/genai-perf/docs/multi_modal.md> Embedding models <../perf_analyzer/genai-perf/docs/embeddings.md> diff --git a/docs/perf_benchmark/model-analyzer-README.rst b/docs/perf_benchmark/model-analyzer-README.rst new file mode 100644 index 0000000000..f31e7ca633 --- /dev/null +++ b/docs/perf_benchmark/model-analyzer-README.rst @@ -0,0 +1,203 @@ +.. +.. Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. +.. Redistribution and use in source and binary forms, with or without +.. modification, are permitted provided that the following conditions +.. are met: +.. * Redistributions of source code must retain the above copyright +.. notice, this list of conditions and the following disclaimer. +.. * Redistributions in binary form must reproduce the above copyright +.. notice, this list of conditions and the following disclaimer in the +.. documentation and/or other materials provided with the distribution. +.. * Neither the name of NVIDIA CORPORATION nor the names of its +.. contributors may be used to endorse or promote products derived +.. from this software without specific prior written permission. +.. +.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +.. EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +.. PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +.. CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +.. EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +.. PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +.. PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +.. OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +.. (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +.. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +.. raw:: html + + +|License| + +Triton Model Analyzer +===================== + + [!Warning] + + .. rubric:: LATEST RELEASE + :name: latest-release + + You are currently on the ``main`` branch which tracks + under-development progress towards the next release. The latest + release of the Triton Model Analyzer is 1.42.0 and is available on + branch + `r24.07 `__. + +Triton Model Analyzer is a CLI tool which can help you find a more +optimal configuration, on a given piece of hardware, for single, +multiple, ensemble, or BLS models running on a `Triton Inference +Server `__. Model +Analyzer will also generate reports to help you better understand the +trade-offs of the different configurations along with their compute and +memory requirements. + +Features +======== + +Search Modes +~~~~~~~~~~~~ + +- `Optuna Search `__ **-ALPHA + RELEASE-** allows you to search for every parameter that can be + specified in the model configuration, using a hyperparameter + optimization framework. Please see the + `Optuna `__ website if you are interested in + specific details on how the algorithm functions. + +- `Quick Search `__ will + **sparsely** search the `Max Batch + Size `__, + `Dynamic + Batching `__, + and `Instance + Group `__ + spaces by utilizing a heuristic hill-climbing algorithm to help you + quickly find a more optimal configuration + +- `Automatic Brute + Search `__ will + **exhaustively** search the `Max Batch + Size `__, + `Dynamic + Batching `__, + and `Instance + Group `__ + parameters of your model configuration + +- `Manual Brute Search `__ + allows you to create manual sweeps for every parameter that can be + specified in the model configuration + +Model Types +~~~~~~~~~~~ + +- `Ensemble `__: Model Analyzer can help + you find the optimal settings when profiling an ensemble model + +- `BLS `__: Model Analyzer can help you find + the optimal settings when profiling a BLS model + +- `Multi-Model `__: Model Analyzer can + help you find the optimal settings when profiling multiple concurrent + models + +- `LLM `__: Model Analyzer can help you find + the optimal settings when profiling Large Language Models + +Other Features +~~~~~~~~~~~~~~ + +- `Detailed and summary reports `__: Model Analyzer is + able to generate summarized and detailed reports that can help you + better understand the trade-offs between different model + configurations that can be used for your model. + +- `QoS Constraints `__: Constraints can help + you filter out the Model Analyzer results based on your QoS + requirements. For example, you can specify a latency budget to filter + out model configurations that do not satisfy the specified latency + threshold. + +Examples and Tutorials +====================== + +**Single Model** +~~~~~~~~~~~~~~~~ + +See the `Single Model Quick Start `__ for a guide +on how to use Model Analyzer to profile, analyze and report on a simple +PyTorch model. + +**Multi Model** +~~~~~~~~~~~~~~~ + +See the `Multi-model Quick Start `__ for a guide +on how to use Model Analyzer to profile, analyze and report on two +models running concurrently on the same GPU. + +**Ensemble Model** +~~~~~~~~~~~~~~~~~~ + +See the `Ensemble Model Quick Start `__ +for a guide on how to use Model Analyzer to profile, analyze and report +on a simple Ensemble model. + +**BLS Model** +~~~~~~~~~~~~~ + +See the `BLS Model Quick Start `__ for a guide +on how to use Model Analyzer to profile, analyze and report on a simple +BLS model. + +Documentation +============= + +- `Installation `__ +- `Model Analyzer CLI `__ +- `Launch Modes `__ +- `Configuring Model Analyzer `__ +- `Model Analyzer Metrics `__ +- `Model Config Search `__ +- `Model Types `__ +- `Checkpointing `__ +- `Model Analyzer Reports `__ +- `Deployment with Kubernetes `__ + +Terminology +=========== + +Below are definitions of some commonly used terms in Model Analyzer: + +- **Model Type** - Category of model being profiled. Examples of this + include single, multi, ensemble, BLS, etc.. +- **Search Mode** - How Model Analyzer explores the possible + configuration space when profiling. This is either exhaustive (brute) + or heuristic (quick/optuna). +- **Model Config Search** - The cross product of model type and search + mode. +- **Launch Mode** - How the Triton Server is deployed and used by Model + Analyzer. + +Reporting problems, asking questions +==================================== + +We appreciate any feedback, questions or bug reporting regarding this +project. When help with code is needed, follow the process outlined in +the Stack Overflow (https://stackoverflow.com/help/mcve) document. +Ensure posted examples are: + +- minimal – use as little code as possible that still produces the same + problem + +- complete – provide all parts needed to reproduce the problem. Check + if you can strip external dependency and still show the problem. The + less time we spend on reproducing problems the more time we have to + fix it + +- verifiable – test the code you’re about to provide to make sure it + reproduces the problem. Remove all other problems that are not + related to your request/question. + +.. |License| image:: https://img.shields.io/badge/License-Apache_2.0-lightgrey.svg + :target: https://opensource.org/licenses/Apache-2.0 diff --git a/docs/perf_benchmark/model_analyzer.rst b/docs/perf_benchmark/model_analyzer.rst index a3c734ce30..c29a96aa92 100644 --- a/docs/perf_benchmark/model_analyzer.rst +++ b/docs/perf_benchmark/model_analyzer.rst @@ -1,5 +1,5 @@ .. -.. Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. .. Redistribution and use in source and binary forms, with or without .. modification, are permitted provided that the following conditions @@ -29,23 +29,17 @@ Model Analyzer #### +.. include:: model-analyzer-README.rst + .. toctree:: :maxdepth: 1 :hidden: - Overview <../model_analyzer/README.md> - Documentation <../model_analyzer/docs/README.md> - Quick Start <../model_analyzer/docs/quick_start.md> - Installation <../model_analyzer/docs/install.md> - CLI Reference <../model_analyzer/docs/cli.md> - Launch Modes <../model_analyzer/docs/launch_modes.md> - Configuration <../model_analyzer/docs/config.md> - Configuration Search <../model_analyzer/docs/config_search.md> - Metrics <../model_analyzer/docs/metrics.md> - Checkpointing <../model_analyzer/docs/checkpoints.md> - Reports <../model_analyzer/docs/report.md> - Kubernetes <../model_analyzer/docs/kubernetes_deploy.md> - Model Types <../model_analyzer/docs/model_types.md> - Ensemble Model <../model_analyzer/docs/ensemble_quick_start.md> - BLS Model <../model_analyzer/docs/bls_quick_start.md> - Multi-Model <../model_analyzer/docs/mm_quick_start.md> \ No newline at end of file + ../model_analyzer/docs/cli.md + ../model_analyzer/docs/launch_modes.md + ../model_analyzer/docs/config.md + ../model_analyzer/docs/metrics.md + ../model_analyzer/docs/config_search.md + ../model_analyzer/docs/checkpoints.md + ../model_analyzer/docs/report.md + ../model_analyzer/docs/kubernetes_deploy.md \ No newline at end of file diff --git a/docs/perf_benchmark/perf-analyzer-README.rst b/docs/perf_benchmark/perf-analyzer-README.rst new file mode 100644 index 0000000000..4f678cfdba --- /dev/null +++ b/docs/perf_benchmark/perf-analyzer-README.rst @@ -0,0 +1,180 @@ +.. +.. Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. +.. Redistribution and use in source and binary forms, with or without +.. modification, are permitted provided that the following conditions +.. are met: +.. * Redistributions of source code must retain the above copyright +.. notice, this list of conditions and the following disclaimer. +.. * Redistributions in binary form must reproduce the above copyright +.. notice, this list of conditions and the following disclaimer in the +.. documentation and/or other materials provided with the distribution. +.. * Neither the name of NVIDIA CORPORATION nor the names of its +.. contributors may be used to endorse or promote products derived +.. from this software without specific prior written permission. +.. +.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +.. EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +.. PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +.. CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +.. EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +.. PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +.. PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +.. OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +.. (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +.. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +.. raw:: html + + +Triton Performance Analyzer +=========================== + +Triton Performance Analyzer is CLI tool which can help you optimize the +inference performance of models running on Triton Inference Server by +measuring changes in performance as you experiment with different +optimization strategies. + +Features +======== + +Inference Load Modes +~~~~~~~~~~~~~~~~~~~~ + +- `Concurrency Mode `__ + simlulates load by maintaining a specific concurrency of outgoing + requests to the server + +- `Request Rate + Mode `__ simulates + load by sending consecutive requests at a specific rate to the server + +- `Custom Interval + Mode `__ simulates + load by sending consecutive requests at specific intervals to the + server + +Performance Measurement Modes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- `Time Windows Mode `__ + measures model performance repeatedly over a specific time interval + until performance has stabilized + +- `Count Windows Mode `__ + measures model performance repeatedly over a specific number of + requests until performance has stabilized + +Other Features +~~~~~~~~~~~~~~ + +- `Sequence Models <../user_guide/architecture.md#stateful-models>`__, + `Ensemble Models <../user_guide/architecture.md#ensemble-models>`__, + and `Decoupled Models <../user_guide/decoupled_models.md>`__ can be + profiled in addition to standard/stateless/coupled models + +- `Input Data `__ to model inferences can be + auto-generated or specified as well as verifying output + +- `TensorFlow + Serving `__ and + `TorchServe `__ can be + used as the inference server in addition to the default Triton server + +Quick Start +=========== + +The steps below will guide you on how to start using Perf Analyzer. + +Step 1: Start Triton Container +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: bash + + export RELEASE= # e.g. to use the release from the end of February of 2023, do `export RELEASE=23.02` + + docker pull nvcr.io/nvidia/tritonserver:${RELEASE}-py3 + + docker run --gpus all --rm -it --net host nvcr.io/nvidia/tritonserver:${RELEASE}-py3 + +Step 2: Download ``simple`` Model +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: bash + + # inside triton container + git clone --depth 1 https://github.com/triton-inference-server/server + + mkdir model_repository ; cp -r server/docs/examples/model_repository/simple model_repository + +Step 3: Start Triton Server +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: bash + + # inside triton container + tritonserver --model-repository $(pwd)/model_repository &> server.log & + + # confirm server is ready, look for 'HTTP/1.1 200 OK' + curl -v localhost:8000/v2/health/ready + + # detach (CTRL-p CTRL-q) + +Step 4: Start Triton SDK Container +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: bash + + docker pull nvcr.io/nvidia/tritonserver:${RELEASE}-py3-sdk + + docker run --gpus all --rm -it --net host nvcr.io/nvidia/tritonserver:${RELEASE}-py3-sdk + +Step 5: Run Perf Analyzer +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: bash + + # inside sdk container + perf_analyzer -m simple + +See the full `quick start guide `__ for additional +tips on how to analyze output. + +Documentation +============= + +- `Installation `__ +- `Perf Analyzer CLI `__ +- `Inference Load Modes `__ +- `Input Data `__ +- `Measurements & Metrics `__ +- `Benchmarking `__ + +Contributing +============ + +Contributions to Triton Perf Analyzer are more than welcome. To +contribute please review the `contribution +guidelines `__, +then fork and create a pull request. + +Reporting problems, asking questions +==================================== + +We appreciate any feedback, questions or bug reporting regarding this +project. When help with code is needed, follow the process outlined in +the Stack Overflow (https://stackoverflow.com/help/mcve) document. +Ensure posted examples are: + +- minimal - use as little code as possible that still produces the same + problem + +- complete - provide all parts needed to reproduce the problem. Check + if you can strip external dependency and still show the problem. The + less time we spend on reproducing problems the more time we have to + fix it + +- verifiable - test the code you’re about to provide to make sure it + reproduces the problem. Remove all other problems that are not + related to your request/question. diff --git a/docs/perf_benchmark/perf_analyzer.rst b/docs/perf_benchmark/perf_analyzer.rst index fcb25e0ca8..d6c6156a62 100644 --- a/docs/perf_benchmark/perf_analyzer.rst +++ b/docs/perf_benchmark/perf_analyzer.rst @@ -1,5 +1,5 @@ .. -.. Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. .. Redistribution and use in source and binary forms, with or without .. modification, are permitted provided that the following conditions @@ -29,16 +29,14 @@ Performance Analyzer #### +.. include:: perf-analyzer-README.rst + .. toctree:: :maxdepth: 1 :hidden: - Overview <../perf_analyzer/README.md> - Documentation <../perf_analyzer/docs/README.md> - Quick Start <../perf_analyzer/docs/quick_start.md> - Installation <../perf_analyzer/docs/install.md> - CLI Reference <../perf_analyzer/docs/cli.md> - Inference Load Modes <../perf_analyzer/docs/inference_load_modes.md> - Input Data <../perf_analyzer/docs/input_data.md> - Measurement Modes <../perf_analyzer/docs/measurements_metrics.md> - Benchmarking <../perf_analyzer/docs/benchmarking.md> \ No newline at end of file + ../perf_analyzer/docs/install.md + ../perf_analyzer/docs/CLI.md + ../perf_analyzer/docs/inference_load_modes.md + ../perf_analyzer/docs/input_data.md + ../perf_analyzer/docs/measurements_metrics.md \ No newline at end of file diff --git a/docs/protocol/extension_parameters.md b/docs/protocol/extension_parameters.md index 17edcf2776..14ed4d1dc5 100644 --- a/docs/protocol/extension_parameters.md +++ b/docs/protocol/extension_parameters.md @@ -1,5 +1,5 @@ -# \ - -**Status**: \[Draft | Under Review | Approved | Replaced | Deferred | Rejected\] - -**Authors**: \[Name/Team\] - -**Category**: \[Architecture | Process | Guidelines\] - -**Replaces**: \[Link of previous proposal if applicable\] - -**Replaced By**: \[Link of previous proposal if applicable\] - -**Sponsor**: \[Name of code owner or maintainer to shepherd process\] - -**Required Reviewers**: \[Names of technical leads that are required for acceptance\] - -**Review Date**: \[Date for review\] - -**Pull Request**: \[Link to Pull Request of the Proposal itself\] - -**Implementation PR / Tracking Issue**: \[Link to Pull Request or Tracking Issue for Implementation\] - -## Summary - -**\[Required\]** - -## Motivation - -**\[Required\]** - -Describe the problem that needs to be addressed with enough detail for someone familiar with the project to understand. -Generally one to two short paragraphs. -Additional details can be placed in the background section as needed. -Cover **what** the issue is and **why** it needs to be addressed. -Link to github issues if relevant. - -### Goals - -**\[Optional \- if not applicable omit\]** - -List out any additional goals in bullet points. -Goals may be aspirational / difficult to measure but guide the proposal. - -* Goal - -* Goal - -* Goal - -### Non Goals - -**\[Optional \- if not applicable omit\]** - -List out any items which are out of scope / specifically not required in bullet points. -Indicates the scope of the proposal and issue being resolved. - -### Requirements - -**\[Optional \- if not applicable omit\]** - -List out any additional requirements in numbered subheadings. - -**\** - -#### REQ \<\#\> \ - -Describe the requirement in as much detail as necessary for others to understand it and how it applies to the TEP. -Keep in mind that requirements should be measurable and will be used to determine if a TEP has been successfully implemented or not. - -Requirement names should be prefixed using a monotonically increasing number such as “REQ 1 \” followed by “REQ 2 \” and so on. -Use title casing when naming requirements. Requirement names should be as descriptive as possible while remaining as terse as possible. - -Use all-caps, bolded terms like **MUST** and **SHOULD** when describing each requirement. -See \[RFC-2119\](https://datatracker.ietf.org/doc/html/rfc2119) for additional information. - -## Proposal - -**\[Required\]** - -Describe the high level design / proposal. -Use sub sections as needed, but start with an overview and then dig into the details. -Try to provide images and diagrams to facilitate understanding. - -## Implementation Details - -**\[Optional \- if not applicable omit\]** - -Add additional detailed items here including interface signatures, etc. -Add anything that is relevant but seems more of a detail than central to the proposal. -Use sub sections / bullet points as needed. -Try to provide images and diagrams to facilitate understanding. -If applicable link to PR. - -### Deferred to Implementation - -**\[Optional \- if not applicable omit\]** - -List out items that are under discussion but that will be resolved only during implementation / code review. - -## Implementation Phases - -**\[Optional \- if not applicable omit\]** - -List out phases of implementation (can be single phase). -Give each phase a monotonically increasing number; example “Phase 0” followed by “Phase 1” and so on. -Give phases titles if it makes sense. - -### Phase \<\#\> \ - -**Release Target**: Date - -**Effort Estimate**: \ - -**Work Item(s):** \ - -**Supported API / Behavior:** - -* \ - -**Not Supported:** - -* \ - -## Related Proposals - -**\[Optional \- if not applicable omit\]** - -* File - -* File - -* File - -* File - -* File - -## Alternate Solutions - -**\[Required, if not applicable write N/A\]** - -List out solutions that were considered but ultimately rejected. -Consider free form `-`, but a possible format shown below. - -### Alt \<\#\> \ - -**Pros:** - -\ - -**Cons:** - -\ - -**Reason Rejected:** - -\ - -**Notes:** - -\ - -## Background - -**\[Optional \- if not applicable omit\]** - -Add additional context and references as needed to help reviewers and authors understand the context of the problem and solution being proposed. - -## References - -**\[Optional \- if not applicable omit\]** - -Add additional references as needed to help reviewers and authors understand the context of the problem and solution being proposed. - -* \ - -## Terminology & Definitions - -**\[Optional \- if not applicable omit\]** - -List out additional terms / definitions (lexicon). -Try to keep definitions as concise as possible and use links to external resources when additional information would be useful to the reader. - -Keep the list of terms sorted alphabetically to ease looking up definitions by readers. - -| \ | \ | -| :---- | :---- | -| **\** | \ | - -## Acronyms & Abbreviations - -**\[Optional \- if not applicable omit\]** - -Provide a list of frequently used acronyms and abbreviations which are uncommon or unlikely to be known by the reader. -Do not include acronyms or abbreviations which the reader is likely to be familiar with. - -Keep the list of acronyms and abbreviations sorted alphabetically to ease looking up definitions by readers. - -Do not include the full definition in the expanded meaning of an abbreviation or acronym. -If the reader needs the definition, please include it in the \[Terminology & Definitions\](#terminology--definitions) section. - -**\:** \ diff --git a/enhancements/NNNN-template-limited.md b/enhancements/NNNN-template-limited.md deleted file mode 100644 index f814dce593..0000000000 --- a/enhancements/NNNN-template-limited.md +++ /dev/null @@ -1,134 +0,0 @@ - -# \ - -**Status**: \[Draft | Under Review | Approved | Replaced | Deferred | Rejected\] - -**Authors**: \[Name/Team\] - -**Category**: \[Architecture | Process | Guidelines\] - -**Replaces**: \[Link of previous proposal if applicable\] - -**Replaced By**: \[Link of previous proposal if applicable\] - -**Sponsor**: \[Name of code owner or maintainer to shepherd process\] - -**Required Reviewers**: \[Names of technical leads that are required for acceptance\] - -**Review Date**: \[Date for review\] - -**Pull Request**: \[Link to Pull Request of the Proposal itself\] - -**Implementation PR / Tracking Issue**: \[Link to Pull Request or Tracking Issue for Implementation\] - -## Summary - -**\[Required\]** - -## Motivation - -**\[Required\]** - -Describe the problem that needs to be addressed with enough detail for someone familiar with the project to understand. -Generally one to two short paragraphs. -Additional details can be placed in the background section as needed. Cover **what** the issue is and **why** it needs to be addressed. -Link to github issues if relevant. - -### Goals - -**\[Optional \- if not applicable omit\]** - -List out any additional goals in bullet points. -Goals may be aspirational / difficult to measure but guide the proposal. - -* Goal - -* Goal - -* Goal - -#### Non Goals - -**\[Optional \- if not applicable omit\]** - -List out any items which are out of scope / specifically not required in bullet points. -Indicates the scope of the proposal and issue being resolved. - -### Requirements - -**\[Optional \- if not applicable omit\]** - -List out any additional requirements in numbered subheadings. - -**\** - -#### REQ \<\#\> \ - -Describe the requirement in as much detail as necessary for others to understand it and how it applies to the TEP. -Keep in mind that requirements should be measurable and will be used to determine if a TEP has been successfully implemented or not. - -Requirement names should be prefixed using a monotonically increasing number such as “REQ 1 \” followed by “REQ 2 \” and so on. -Use title casing when naming requirements. -Requirement names should be as descriptive as possible while remaining as terse as possible. - -Use all-caps, bolded terms like **MUST** and **SHOULD** when describing each requirement. -See \[RFC-2119\](https://datatracker.ietf.org/doc/html/rfc2119) for additional information. - -## Proposal - -**\[Required\]** - -Describe the high level design / proposal. -Use sub sections as needed, but start with an overview and then dig into the details. -Try to provide images and diagrams to facilitate understanding. - -## Alternate Solutions - -**\[Required, if not applicable write N/A\]** - -List out solutions that were considered but ultimately rejected. -Consider free form `-`, but a possible format shown below. - -## Alt \<\#\> \ - -**Pros:** - -\ - -**Cons:** - -\ - -**Reason Rejected:** - -\ - -**Notes:** - -\ diff --git a/enhancements/README.md b/enhancements/README.md deleted file mode 100644 index 6cf8c3a1da..0000000000 --- a/enhancements/README.md +++ /dev/null @@ -1,44 +0,0 @@ - -# Triton Enhancement Proposals (TEP) - -Enhancement Proposals and Architecture Decisions - -Please see [0000-tep-process](teps/0000-tep-process.md) for full explanation and details. - -## Authoring Guidelines - -1. Start with either the: -- [NNNN-template-complete.md](NNNN-template-complete.md) and remove unneeded sections. -- [NNNN-template-limited.md](NNNN-template-limited.md) and then add selectively from the complete template based on need. - -1. Identify a **Code-Owner** or **Maintainer** of the TEP repository to shepherd the process. - -2. Create a draft PR and iterate with co-authors, **Sponsor** - -3. When ready for review, mark as ready and work with **Sponsor** to set a **Review Date**. diff --git a/enhancements/teps/0000-tep-process.md b/enhancements/teps/0000-tep-process.md deleted file mode 100644 index b85c538dcf..0000000000 --- a/enhancements/teps/0000-tep-process.md +++ /dev/null @@ -1,265 +0,0 @@ - -# Triton Enhancement Proposals - -**Status**: Draft - -**Authors**: [whoisj](https://github.com/whoisj) - -**Category**: Process - -**Replaces**: N/A - -**Replaced By**: N/A - -**Sponsor**: [whoisj](https://github.com/whoisj) - -**Required Reviewers**: [dzier](https://github.com/dzier), [nnshah1](https://github.com/nnshah1) - -**Review Date**: 17 Oct 2025 - -**Pull Request**: [N/A](https://github.com/triton-inference-server/server/pull/8517) - -## Summary - -A standard process and format for proposing and capturing architecture, design, and process decisions for the Triton project along with the motivations behind those decisions. -We adopt a similar process as adopted by Dynamo, Kubernetes, Rust, Python, and Ray broadly categorized as "enhancement proposals". - -## Motivation - -With any software project but especially agile, open source projects in the AI space, architecture, design, and process decisions are made rapidly and for specific reasons which can sometimes be difficult to understand after the fact. -For Triton in particular many teams and community members are collaborating for the first time and have varied backgrounds and design philosophies. -The Triton project's code base itself reflects multiple previously independent code bases integrated quickly to meet overall project goals. -As the project evolves we need a way to propose, ratify and capture architecture, design and process decisions quickly and thoughtfully in a transparent, consistent, lightweight, maintainable way. - -Borrowing from the motivation for KEPs: - -> The purpose of the KEP process is to reduce the amount of "tribal knowledge" in our community. -> By moving decisions from a smattering of mailing lists, video calls and hallway conversations into a well tracked artifact, this process aims to enhance communication and discoverability. - -### Goals - -* **Useful** - - Enhancement proposals and the process of writing and approving them should encourage the thoughtful evaluation of design, process, and architecture choices and lead to timely decisions with a clear record of what was decided, why, and what other options were considered. - -* **Lightweight and Scalable** - - The format and process should be applicable both to small or medium sized changes as well as large ones. - The process should not impede the rate of progress but serve to provide timely feedback, discussion, and ratification on key proposals. - The process should also support retroactive documents to capture and explain decisions already made. - -* **Single Document for Requirements and Design** - - Combine aspects of requirements documents, design documents and software architecture documents into a single document. - Give one place to understand the motivation, requirements, and design of a feature or process. - -* **Support Process, Architecture and Guideline Decisions** - - Have a single format to articulate decisions that effect process (such as github merge rules or templates) as well as code and design guidelines as well as features. - -* **Clear** - - Should be relatively clear when a document is required, when the review needs to be completed, and by who and what the overall process is. - -* **Encourage Collaboration** - - Should allow for easy collaboration and communication between *Authors** and **Reviewers**. - -* **Flexible** - - Format and process should be flexible enough to be used for different types of decisions requiring different levels of detail and formatting of sections. - -### Non Goals - -* Triton Enhancement Proposals (TEP)s do not take the place of other forms of documentation such as user / developer facing documentation (including architecture documents, api documentation) -* Prototyping and early development are not gated by design / architectural approval. -* TEPs should not be a perfunctory process but lead to discussion and thought process around good designs. -* Not all changes (bug fixes, documentation improvements) need a TEP - and many can be reviewed via that normal GitHub pull request - -## Proposal - -Following successful open source projects such as Kubernetes (KEP) and Dynamo (DEP) we adopt a markdown based enhancement proposal format designed to support any decisions we need to capture as a project. -We will adopt an open, community-wide, discussion and comment process using pull requests but enable **Code-Owners** and **Maintainers** to be the final arbiters of **Approval**. - -Subject area experts will be listed as required **Reviewers** to ensure proposals are complete and reviewed properly. - - - -We provide two templates "limited" and "complete" where the limited template is a strict subset of the complete template, and both indicate which sections are required and which are optional. - -## Implementation Details - -### Proposal Process - - - -* Copy the [limited template](../NNNN-template-limited.md) or [complete template](../NNNN-template-complete.md) to `teps/NNNN-my-feature.md` (where `my-feature` is descriptive, don't assign an `TEP` identifier yet) - - > [!Note] - > Choose the template that fits your purpose. - > You can start with the limited form and pull additional sections from the complete form as needed. - > Keep the order of the sections consistent. - -* Identify a **Sponsor** from the list of **Maintainers** or **Code-Owners** to help with the process. - -* Fill in the proposal template. - Be sure to include all required sections. - Keep sections in the order prescribed in the template. - -* Work with the **Sponsor** to identify the required reviewers and a timeline for review. - - - -* If discussion is needed the **Sponsor** can ask for a slot in the weekly Engineering Sync or schedule an ad-hoc meeting with the required reviewers. - -* Iterate and incorporate feedback via the pull request. - -* When review is complete The **Sponsor** will merge the request and update the status. - -* **Sponsor** should assign an identifier. - -* **Author** and **Sponsor** should add issues and/or PRs as needed to track implementation. - -### When is a proposal required? - -It is difficult to enumerate all the circumstances where a proposal would be required or not required. -Generally we will follow this process when making "substantial changes". -The definition of "substantial" is evolving and mainly determined by the core team and community. - -When in doubt reach out to a **Maintainer** or **Code-Owner**. - -**Generally speaking a proposal would not be required for**: - -* Bug fixes that don't change advertised behavior - -* Documentation fixes / updates - -* Minor refactors within a single module - -**Generally speaking proposals would be required for**: - -* New features which add significant functionality - -* Changes to existing features or code which require discussion - -* Changes to public interfaces - -* Responses to security related vulnerabilities found directly in the project code - -* Changes to packaging and installation - -* When a **Maintainer** or **Code-Owner** recommends that a change go through the proposal process - -* Retroactively to capture current architecture, guideline, or process - -### Minor Changes After Review - -For minor changes or changes that are in the spirit of the review, updates can be made to the document without a new proposal. - -*Example:* links to implementation - -### Significant Changes After Review - -For significant changes, a new proposal should be made and the original marked as replaced. - -### Maintenance - -TEPs should be reviewed for updates, replacements, or archiving on a regular basis. - -### Sensitive Changes and Discussions - -Certain types of changes need to be discussed and ratified before being made public due to timing of non-disclosed information. -In such (rare) cases, drafts and reviews will be conducted offline by **Authors**, **Code-Owners**, and **Maintainers** with the public proposals being updated when possible. - -*Example:* when responding to undisclosed security vulnerabilities, we want to avoid inadvertently encouraging zero day attacks for deployed systems. - -In such (rare) cases, we may make use of a private repo on a temporary basis to collect feedback before publishing to the public repo. - -### Deferred to Implementation - -* Definition of **Code-Owners** and **Maintainers** - -* Whether or not to organize **TEP**s into sub directories for projects / areas - -* Tooling around the creation / indexing of **TEP**s - -* Making requirements required in addition to motivation - -* Format recommendations for API surfaces / other formatted components. - -* Decisions / guidelines on when a TEP is needed. - -## Alternate Solutions - -### Alt 1 Google Docs - -**Pros:** - -* Fits existing documents and templates used by many teams - -**Cons:** - -* Difficult to integrate with AI tools. - -* Difficult to search and index - -**Reason Rejected:** - -* Want to standardize around a simple text format and use AI tools also for diagramming, etc. - -## Background - -With the rise of Agile software development practices and large open source projects, software development teams needed to devise new and lightweight (w.r.t to previous software architecture documents) ways of recording architecture proposals and decisions. -As Agile was born in part as a reaction to waterfall styles of planning and development and famously prioritized “Working software over comprehensive documentation”, so too there was a need to replace monolithic large software design specifications with something lighter weight but that still encouraged good architecture. - -From this need for a new way of practicing software architecture a body of work and theory has evolved around the concepts of “Architecture Decision Records” which in turn are also termed “Any Decision Record”, and RFCs or Enhancement proposals (PEP, KEP, REP). - -In each case the core requirements of the process are that the team document the problem, the proposal / design, the status of the proposal, implications / follow on work, and any alternatives that were considered using a standard template and review process. - -Just as in Agile planning, each team modifies the template and process to fit their needs. - -### References - -1. [Documenting Architecture Decisions (cognitect.com)](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions) - -2. [The most plagiarized Architecture Decision Record blog on the internet. | by Conall Daly | Medium](https://conalldalydev.medium.com/the-most-plagiarised-architecture-decision-record-blog-on-the-internet-c9dd2018c1d6) - -3. [adr.github.io](https://adr.github.io/) - -4. [When Should I Write an Architecture Decision Record \- Spotify Engineering : Spotify Engineering (atspotify.com)](https://engineering.atspotify.com/2020/04/when-should-i-write-an-architecture-decision-record/) - -5. [Scaling Engineering Teams via RFCs: Writing Things Down \- The Pragmatic Engineer](https://blog.pragmaticengineer.com/scaling-engineering-teams-via-writing-things-down-rfcs/) - -6. [Love Unrequited: The Story of Architecture, Agile, and How Architecture Decision Records Brought Them Together | IEEE Journals & Magazine | IEEE Xplore](https://ieeexplore.ieee.org/document/9801811) - -7. [ray-project/enhancements: Tracking Ray Enhancement Proposals (github.com)](https://github.com/ray-project/enhancements) - -8. [Kubernetes Enhancement Proposals](https://github.com/kubernetes/enhancements/blob/master/keps/sig-architecture/0000-kep-process/README.md) - -9. [Dynamo Enhancement Proposals](https://github.com/ai-dynamo/enhancements/blob/main/README.md) diff --git a/python/openai/README.md b/python/openai/README.md index 92d91d4db7..53a9f461f4 100644 --- a/python/openai/README.md +++ b/python/openai/README.md @@ -1,5 +1,5 @@ -# OpenAI-Compatible Frontend for Triton Inference Server +# OpenAI-Compatible Frontend for Triton Inference Server (Beta) + +> [!NOTE] +> The OpenAI-Compatible API is currently in BETA. Its features and functionality +> are subject to change as we collect feedback. We're excited to hear any thoughts +> you have and what features you'd like to see! ## Pre-requisites @@ -38,7 +43,7 @@ ## VLLM 1. Launch the container and install dependencies: - - Mounts the `~/.cache/huggingface` for re-use of downloaded models across runs, containers, etc. + - Mounts the `~/.huggingface/cache` for re-use of downloaded models across runs, containers, etc. - Sets the [`HF_TOKEN`](https://huggingface.co/docs/huggingface_hub/en/package_reference/environment_variables#hftoken) environment variable to access gated models, make sure this is set in your local environment if needed. @@ -46,7 +51,7 @@ docker run -it --net=host --gpus all --rm \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ -e HF_TOKEN \ - nvcr.io/nvidia/tritonserver:26.05-vllm-python-py3 + nvcr.io/nvidia/tritonserver:25.03-vllm-python-py3 ``` 2. Launch the OpenAI-compatible Triton Inference Server: @@ -93,7 +98,7 @@ curl -s http://localhost:9000/v1/chat/completions -H 'Content-Type: application/ ```json { - "id": "cmpl-0242093d-51ae-11f0-b339-e7480668bfbe", + "id": "cmpl-6930b296-7ef8-11ef-bdd1-107c6149ca79", "choices": [ { "finish_reason": "stop", @@ -108,15 +113,11 @@ curl -s http://localhost:9000/v1/chat/completions -H 'Content-Type: application/ "logprobs": null } ], - "created": 1750846825, + "created": 1727679085, "model": "llama-3.1-8b-instruct", "system_fingerprint": null, "object": "chat.completion", - "usage": { - "completion_tokens": 7, - "prompt_tokens": 42, - "total_tokens": 49 - } + "usage": null } ``` @@ -137,32 +138,28 @@ curl -s http://localhost:9000/v1/completions -H 'Content-Type: application/json' ```json { - "id": "cmpl-58fba3a0-51ae-11f0-859d-e7480668bfbe", + "id": "cmpl-d51df75c-7ef8-11ef-bdd1-107c6149ca79", "choices": [ { "finish_reason": "stop", "index": 0, "logprobs": null, - "text": " an amazing field that can truly understand the hidden patterns that exist in the data," + "text": " a field of computer science that focuses on developing algorithms that allow computers to learn from" } ], - "created": 1750846970, + "created": 1727679266, "model": "llama-3.1-8b-instruct", "system_fingerprint": null, "object": "text_completion", - "usage": { - "completion_tokens": 16, - "prompt_tokens": 4, - "total_tokens": 20 - } + "usage": null } ```
5. Benchmark with `genai-perf`: -- To install genai-perf in this container, see the instructions [here](https://github.com/triton-inference-server/perf_analyzer/tree/main/genai-perf#install-genai-perf-ubuntu-2404-python-310) -- Or try using genai-perf from the [SDK container](https://catalog.ngc.nvidia.com/orgs/nvidia/containers/tritonserver) +- To install genai-perf in this container, see the instructions [here](https://github.com/triton-inference-server/perf_analyzer/tree/main/genai-perf#install-perf-analyzer-ubuntu-python-38) +- Or try using genai-perf from the [SDK container](https://github.com/triton-inference-server/perf_analyzer/tree/main/genai-perf#install-perf-analyzer-ubuntu-python-38) ```bash MODEL="llama-3.1-8b-instruct" @@ -219,7 +216,7 @@ completion = client.chat.completions.create( }, {"role": "user", "content": "What are LLMs?"}, ], - max_completion_tokens=256, + max_tokens=256, ) print(completion.choices[0].message.content) @@ -233,208 +230,6 @@ pip install -r requirements-test.txt pytest -v tests/ ``` -### LoRA Adapters - -If the command line argument `--lora-separator=` is provided -when starting the OpenAI Frontend, a LoRA adaptor listed in `multi_lora.json` -may be selected by appending the LoRA name to the model name, -separated by the LoRA separator, on the inference request in -`` format. - -
-For example - -```bash -# start server with model named gemma-2b -python3 openai_frontend/main.py --lora-separator=_lora_ ... - -# inference without LoRA -curl -s http://localhost:9000/v1/completions -H 'Content-Type: application/json' -d '{ - "model": "gemma-2b", - "temperature": 0, - "prompt": "When was the wheel invented?" -}' -{ - ... - "choices":[{..."text":"\n\nThe wheel was invented by the Sumerians in Mesopotamia around 350"}], - ... -} - -# inference with LoRA named doll -curl -s http://localhost:9000/v1/completions -H 'Content-Type: application/json' -d '{ - "model": "gemma-2b_lora_doll", - "temperature": 0, - "prompt": "When was the wheel invented?" -}' -{ - ... - "choices":[{..."text":"\n\nThe wheel was invented in Mesopotamia around 3500 BC.\n\n"}], - ... -} - -# inference with LoRA named sheep -curl -s http://localhost:9000/v1/completions -H 'Content-Type: application/json' -d '{ - "model": "gemma-2b_lora_sheep", - "temperature": 0, - "prompt": "When was the wheel invented?" -}' -{ - ... - "choices":[{..."text":"\n\nThe wheel was invented around 3000 BC in Mesopotamia.\n\n"}], - ... -} -``` - -
- -When listing or retrieving model(s), the model id will include the LoRA name in -the same `` format for each LoRA -adapter listed on the `multi_lora.json`. Note: The LoRA name inclusion is -limited to locally stored models, inference requests are not limited though. - -#### vLLM -See the -[vLLM documentation](https://github.com/triton-inference-server/vllm_backend/blob/main/docs/llama_multi_lora_tutorial.md) -on how to serve a vLLM model with LoRA adapters. - -#### TensorRT-LLM -Similarly, see [TensorRT-LLM document](https://github.com/triton-inference-server/tensorrtllm_backend/blob/main/docs/lora.md) -on how to prepare LoRA-enabled TensorRT-LLM engines and generate LoRA tensors. -The path of LoRA adapter in `multi_lora.json` is the directory of -`model.lora_config.npy` and `model.lora_weights.npy` tensors. - -
-For example - -model repository -``` -inflight_batcher_llm -├── postprocessing -| ├── 1 -| | └── model.py -| └── config.pbtxt -├── preprocessing -| ├── 1 -| | └── model.py -| └── config.pbtxt -├── tensorrt_llm -| ├── 1 -| | └── model.py -| └── config.pbtxt -└── tensorrt_llm_bls - ├── 1 - | ├── Japanese-Alpaca-LoRA-7b-v0-weights - | | ├── model.lora_config.npy - | | └── model.lora_weights.npy - | ├── luotuo-lora-7b-0.1-weights - | | ├── model.lora_config.npy - | | └── model.lora_weights.npy - | ├── model.py - | └── multi_lora.json - └── config.pbtxt -``` - -multi_lora.json -``` -{ - "doll": "inflight_batcher_llm/tensorrt_llm_bls/1/luotuo-lora-7b-0.1-weights", - "sheep": "inflight_batcher_llm/tensorrt_llm_bls/1/Japanese-Alpaca-LoRA-7b-v0-weights" -} -``` -
- -### Embedding Models -Currently, OpenAI-Compatible Frontend supports loading embedding models and embeddings endpoints via vLLM backend. Check [vLLM supported models](https://docs.vllm.ai/en/latest/models/supported_models.html#embedding) for all supported embedding models from vLLM. - -1. Launch the container and install dependencies: - - Mounts the `~/.cache/huggingface` for re-use of downloaded models across runs, containers, etc. - - Sets the [`HF_TOKEN`](https://huggingface.co/docs/huggingface_hub/en/package_reference/environment_variables#hftoken) environment variable to - access gated models, make sure this is set in your local environment if needed. - -```bash -docker run -it --net=host --gpus all --rm \ - -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ - -e HF_TOKEN \ - nvcr.io/nvidia/tritonserver:26.05-vllm-python-py3 -``` - -2. Launch the OpenAI-compatible Triton Inference Server: -```bash -cd /opt/tritonserver/python/openai - -# NOTE: Embeddings endpoint does not require "--tokenizer" -python3 openai_frontend/main.py --model-repository tests/vllm_embedding_models -``` - -
-Example output - -``` -... -+------------------+---------+--------+ -| Model | Version | Status | -+------------------+---------+--------+ -| all-MiniLM-L6-v2 | 1 | READY | <- Correct Model Loaded in Triton -+------------------+---------+--------+ -... -Found model: name='all-MiniLM-L6-v2', backend='vllm' -[WARNING] Adding CORS for the following origins: ['http://localhost'] -INFO: Started server process [133] -INFO: Waiting for application startup. -INFO: Application startup complete. -INFO: Uvicorn running on http://0.0.0.0:9000 (Press CTRL+C to quit) <- OpenAI Frontend Started Successfully -``` - -
- -3. Send a `/v1/embeddings` request: - - Note the use of `jq` is optional, but provides a nicely formatted output for JSON responses. -```bash -MODEL="all-MiniLM-L6-v2" -curl -s http://localhost:9000/v1/embeddings \ - -H 'Content-Type: application/json' \ - -d '{ - "model": "'${MODEL}'", - "input": "The food was delicious and the waiter...", - "dimensions": 10, - "encoding_format": "float" - }' | jq -``` - -
-Example output - -```json -{ - "object": "list", - "data": [ - { - "object": "embedding", - "embedding": [ - -0.1914404183626175, - 0.4000193178653717, - 0.058502197265625, - 0.18909454345703125, - -0.4690297544002533, - 0.004936536308377981, - 0.45893096923828125, - -0.31141534447669983, - 0.18299102783203125, - -0.4907582700252533 - ], - "index": 0 - } - ], - "model": "all-MiniLM-L6-v2", - "usage": { - "prompt_tokens": 12, - "total_tokens": 12 - } -} -``` - -
- ## TensorRT-LLM 0. Prepare your model repository for a TensorRT-LLM model, build the engine, etc. You can try any of the following options: @@ -442,7 +237,7 @@ curl -s http://localhost:9000/v1/embeddings \ - [TRT-LLM Backend Quickstart](https://github.com/triton-inference-server/tensorrtllm_backend?tab=readme-ov-file#quick-start) 1. Launch the container: - - Mounts the `~/.cache/huggingface` for re-use of downloaded models across runs, containers, etc. + - Mounts the `~/.huggingface/cache` for re-use of downloaded models across runs, containers, etc. - Sets the [`HF_TOKEN`](https://huggingface.co/docs/huggingface_hub/en/package_reference/environment_variables#hftoken) environment variable to access gated models, make sure this is set in your local environment if needed. @@ -451,7 +246,7 @@ docker run -it --net=host --gpus all --rm \ -v ${HOME}/.cache/huggingface:/root/.cache/huggingface \ -e HF_TOKEN \ -e TRTLLM_ORCHESTRATOR=1 \ - nvcr.io/nvidia/tritonserver:26.05-trtllm-python-py3 + nvcr.io/nvidia/tritonserver:24.11-trtllm-python-py3 ``` 2. Install dependencies inside the container: @@ -488,13 +283,13 @@ curl -s http://localhost:9000/v1/chat/completions -H 'Content-Type: application/ ```json { - "id": "cmpl-5ad4f860-bf13-11f0-b137-b75b7f0a8586", + "id": "cmpl-704c758c-8a84-11ef-b106-107c6149ca79", "choices": [ { "finish_reason": "stop", "index": 0, "message": { - "content": "It looks like you're ready to see if I'm functioning properly. What would", + "content": "It looks like you're testing the system!", "tool_calls": null, "role": "assistant", "function_call": null @@ -502,15 +297,11 @@ curl -s http://localhost:9000/v1/chat/completions -H 'Content-Type: application/ "logprobs": null } ], - "created": 1762875029, - "model": "tensorrt_llm_bls", + "created": 1728948689, + "model": "llama-3-8b-instruct", "system_fingerprint": null, "object": "chat.completion", - "usage": { - "prompt_tokens": 42, - "total_tokens": 58, - "completion_tokens": 16 - } + "usage": null } ``` @@ -542,125 +333,6 @@ available arguments and default values. For more information on the `tritonfrontend` python bindings, see the docs [here](https://github.com/triton-inference-server/server/blob/main/docs/customization_guide/tritonfrontend.md). -## Model Management - -The OpenAI-compatible frontend supports explicit model control, allowing you to -dynamically load and unload models at runtime without restarting the server. -This is particularly useful when hosting multiple large models on a shared GPU -cluster and needing to swap models on demand. - -### Model Control Mode - -Use `--model-control-mode` to specify how models are managed at startup and -runtime. The default is `none`. - -| Mode | Behavior | -|------|----------| -| `none` (default) | All models in the repository are loaded at startup. Load/unload APIs are not available. | -| `explicit` | No models are loaded at startup unless specified with `--load-model`. Load and unload are controlled via the management API. | - -> [!NOTE] -> This matches the native `tritonserver --model-control-mode` behavior. -> See [Triton Model Management](../../docs/user_guide/model_management.md) for -> more details. - -### Load Models at Startup (Explicit Mode) - -When using `--model-control-mode=explicit`, use `--load-model` to specify which -models should be loaded at startup. It may be specified multiple times to load -multiple models. - -
-Example - -```bash -# Start in explicit mode with no models loaded -python3 openai_frontend/main.py \ - --model-repository /path/to/models \ - --tokenizer meta-llama/Meta-Llama-3.1-8B-Instruct \ - --model-control-mode explicit - -# Start in explicit mode and load a specific model at startup -python3 openai_frontend/main.py \ - --model-repository /path/to/models \ - --tokenizer meta-llama/Meta-Llama-3.1-8B-Instruct \ - --model-control-mode explicit \ - --load-model llama-3.1-8b-instruct - -# Load multiple models at startup -python3 openai_frontend/main.py \ - --model-repository /path/to/models \ - --tokenizer meta-llama/Meta-Llama-3.1-8B-Instruct \ - --model-control-mode explicit \ - --load-model model-a \ - --load-model model-b - -# Load ALL models in the repository at startup -python3 openai_frontend/main.py \ - --model-repository /path/to/models \ - --tokenizer meta-llama/Meta-Llama-3.1-8B-Instruct \ - --model-control-mode explicit \ - --load-model '*' -``` - -
- -> [!IMPORTANT] -> - `--load-model` requires `--model-control-mode=explicit`. -> - `--load-model=*` can not be used together with loading specific models. - -### Dynamic Load / Unload API - -Once the server is running in `explicit` mode, models can be loaded and unloaded -at runtime via the following endpoints: - -| Method | Endpoint | Description | -|--------|----------|-------------| -| `POST` | `/v1/models/{model_name}/load` | Load a model. Blocks until model is fully loaded and ready. | -| `POST` | `/v1/models/{model_name}/unload` | Unload a model. Blocks until fully unloaded. In-flight requests complete before removal. | - -Both endpoints return an error if `--model-control-mode` is not `explicit`. - -#### Load a model - -```bash -MODEL="llama-3.1-8b-instruct" -curl -s -X POST http://localhost:9000/v1/models/${MODEL}/load | jq -``` - -
-Example output - -```json -{ - "id": "llama-3.1-8b-instruct", - "object": "model", - "created": 1750000000, - "owned_by": "Triton Inference Server" -} -``` - -
- -#### Unload a model - -```bash -MODEL="llama-3.1-8b-instruct" -curl -s -X POST http://localhost:9000/v1/models/${MODEL}/unload | jq -``` - -
-Example output - -```json -{ - "status": "success", - "model": "llama-3.1-8b-instruct" -} -``` - -
- ## Model Parallelism Support - [x] vLLM ([EngineArgs](https://github.com/triton-inference-server/vllm_backend/blob/main/README.md#using-the-vllm-backend)) @@ -670,307 +342,3 @@ curl -s -X POST http://localhost:9000/v1/models/${MODEL}/unload | jq - Set the following environment variable: `export TRTLLM_ORCHESTRATOR=1` - [ ] TensorRT-LLM ([Leader Mode](https://github.com/triton-inference-server/tensorrtllm_backend/blob/main/README.md#leader-mode)) - Not currently supported - -## Tool Calling - -The OpenAI frontend supports `tools` and `tool_choice` in the `v1/chat/completions` API. Please refer to the OpenAI API reference for more details about these parameters: - [tools](https://platform.openai.com/docs/api-reference/chat/create#chat-create-tools), - [tool_choice](https://platform.openai.com/docs/api-reference/chat/create#chat-create-tool_choice) - -To enable the tool-calling feature, add the `--tool-call-parser {parser_name}` flag when starting the server. The two available parsers are `llama3` and `mistral`. -The `llama3` parser supports tool-calling features for LLaMA 3.1, 3.2, and 3.3 models, while the `mistral` parser supports tool-calling features for the Mistral Instruct model. - -Example for launching the OpenAI frontend with a tool call parser: -``` -python3 openai_frontend/main.py \ - --model-repository tests/vllm_models \ - --tokenizer meta-llama/Meta-Llama-3.1-8B-Instruct \ - --tool-call-parser llama3 -``` - -Example for making a tool calling request: - -```python -import json -from openai import OpenAI - - -def get_current_weather(city: str, state: str, unit: "str"): - return ( - "The weather in Dallas, Texas is 85 degrees fahrenheit. It is " - "partly cloudly, with highs in the 90's." - ) - -available_tools = {"get_current_weather": get_current_weather} - -openai_api_key = "EMPTY" -openai_api_base = "http://localhost:9000/v1" - -client = OpenAI( - api_key=openai_api_key, - base_url=openai_api_base, -) - -model = "llama-3.1-8b-instruct" # change this to the model in the repository - -tools = [ - { - "type": "function", - "function": { - "name": "get_current_weather", - "description": "Get the current weather in a given location", - "parameters": { - "type": "object", - "properties": { - "city": { - "type": "string", - "description": "The city to find the weather for, e.g. 'San Francisco'", - }, - "state": { - "type": "string", - "description": "the two-letter abbreviation for the state that the city is" - " in, e.g. 'CA' which would mean 'California'", - }, - "unit": { - "type": "string", - "description": "The unit to fetch the temperature in", - "enum": ["celsius", "fahrenheit"], - }, - }, - "required": ["city", "state", "unit"], - }, - }, - } -] - -messages = [ - { - "role": "system", - "content": "You're a helpful assistant! Answer the users question best you can.", - }, - {"role": "user", "content": "What is the weather in Dallas, Texas in Fahrenheit?"}, -] - -tool_calls = client.chat.completions.create( - messages=messages, model=model, tools=tools, max_completion_tokens=128 -) -function_name = tool_calls.choices[0].message.tool_calls[0].function.name -function_arguments = tool_calls.choices[0].message.tool_calls[0].function.arguments - -print(f"function name: " f"{function_name}") -print(f"function arguments: {function_arguments}") -print(f"tool calling result: {available_tools[function_name](**json.loads(function_arguments))}") -``` - -Example output: -``` -function name: get_current_weather -function arguments: {"city": "Dallas", "state": "TX", "unit": "fahrenheit"} -tool calling result: The weather in Dallas, Texas is 85 degrees fahrenheit. It is partly cloudly, with highs in the 90's. -``` - -#### Named Tool Calling - -The OpenAI frontend supports named function calling, utilizing structured outputs in the vLLM backend and guided decoding in TensorRT-LLM backend. Users can specify one of the tools in `tool_choice` to force the model to select a specific tool for function calling. - -> [!NOTE] -> For instructions on enabling guided decoding in the TensorRT-LLM backend, please refer to [this guide](https://github.com/triton-inference-server/tensorrtllm_backend/blob/main/docs/guided_decoding.md) - -Example for making a named tool calling request: - -```python -import json -from openai import OpenAI - - -def get_current_weather(city: str, state: str, unit: "str"): - return ( - "The weather in Dallas, Texas is 85 degrees fahrenheit. It is " - "partly cloudly, with highs in the 90's." - ) - -def get_n_day_weather_forecast(city: str, state: str, unit: str, num_days: int): - return ( - f"The weather in Dallas, Texas is 85 degrees fahrenheit in next {num_days} days." - ) - -available_tools = {"get_current_weather": get_current_weather, - "get_n_day_weather_forecast": get_n_day_weather_forecast} - -openai_api_key = "EMPTY" -openai_api_base = "http://localhost:9000/v1" -client = OpenAI( - api_key=openai_api_key, - base_url=openai_api_base, -) -model = "llama-3.1-8b-instruct" # change this to the model in the repository -tools = [ - { - "type": "function", - "function": { - "name": "get_current_weather", - "description": "Get the current weather in a given location", - "parameters": { - "type": "object", - "properties": { - "city": { - "type": "string", - "description": "The city to find the weather for, e.g. 'San Francisco'", - }, - "state": { - "type": "string", - "description": "the two-letter abbreviation for the state that the city is" - " in, e.g. 'CA' which would mean 'California'", - }, - "unit": { - "type": "string", - "description": "The unit to fetch the temperature in", - "enum": ["celsius", "fahrenheit"], - }, - }, - "required": ["city", "state", "unit"], - }, - }, - }, - { - "type": "function", - "function": { - "name": "get_n_day_weather_forecast", - "description": "Get an N-day weather forecast", - "parameters": { - "type": "object", - "properties": { - "city": { - "type": "string", - "description": "The city to find the weather for, " - "e.g. 'San Francisco'", - }, - "state": { - "type": "string", - "description": "must the two-letter abbreviation for the state " - "that the city is in, e.g. 'CA' which would " - "mean 'California'", - }, - "unit": { - "type": "string", - "description": "The unit to fetch the temperature in", - "enum": ["celsius", "fahrenheit"], - }, - "num_days": { - "type": "integer", - "description": "The number of days to forecast", - }, - }, - "required": ["city", "state", "unit", "num_days"], - }, - }, - } -] - -tool_choice = {"function": {"name": "get_n_day_weather_forecast"}, "type": "function"} - -messages = [ - { - "role": "system", - "content": "You're a helpful assistant! Answer the users question best you can.", - }, - {"role": "user", "content": "What is the weather in Dallas, Texas in Fahrenheit?"}, -] - -tool_calls = client.chat.completions.create( - messages=messages, model=model, tools=tools, tool_choice=tool_choice, max_completion_tokens=128 -) -function_name = tool_calls.choices[0].message.tool_calls[0].function.name -function_arguments = tool_calls.choices[0].message.tool_calls[0].function.arguments - -print(f"function name: {function_name}") -print(f"function arguments: {function_arguments}") -print(f"tool calling result: {available_tools[function_name](**json.loads(function_arguments))}") -``` - -Example output: -``` -function name: get_n_day_weather_forecast -function arguments: {"city": "Dallas", "state": "TX", "unit": "fahrenheit", num_days: 1} -tool calling result: The weather in Dallas, Texas is 85 degrees fahrenheit in next 1 days. -``` - -## Limit Endpoint Access - -The OpenAI-compatible server supports restricting access to specific API endpoints through authentication headers. This feature allows you to protect sensitive endpoints while keeping others publicly accessible. - -### Configuration - -Use the `--openai-restricted-api` command-line argument to configure endpoint restrictions: - -``` ---openai-restricted-api ,,... -``` - -- **`API`**: A comma-separated list of APIs to be included in this group. Note that currently a given API is not allowed to be included in multiple groups. The following protocols / APIs are recognized: - - **inference**: Chat completions and text completions endpoints - - `POST /v1/chat/completions` - - `POST /v1/completions` - - **embedding**: Embedding endpoint - - `POST /v1/embeddings` - - **model-repository**: Model listing, information, and dynamic load/unload endpoints - - `GET /v1/models` - - `GET /v1/models/{model_name}` - - `POST /v1/models/{model_name}/load` - - `POST /v1/models/{model_name}/unload` - - **metrics**: Server metrics endpoint - - `GET /metrics` - - **health**: Health check endpoint - - `GET /health/ready` - -- **`restricted-key`**: The HTTP request header to be checked when a request is received. -- **`restricted-value`**: The header value required to access the specified protocols. - -### Examples - -#### Restrict Inference API Endpoints Only -```bash ---openai-restricted-api "inference api-key my-secret-key" -``` - -Clients must include the header: -```bash -curl -H "api-key: my-secret-key" \ - -X POST http://localhost:9000/v1/chat/completions \ - -d '{"model": "my-model", "messages": [{"role": "user", "content": "Hello"}]}' -``` - -#### Restrict Multiple API Endpoints -```bash -# Different authentication for different APIs ---openai-restricted-api "inference user-key user-secret" \ ---openai-restricted-api "model-repository admin-key admin-secret" - -# Multiple APIs in single argument with shared authentication ---openai-restricted-api "inference,model-repository shared-key shared-secret" -``` - -## HTTP Request Body Size Limit - -The frontend enforces a maximum request body size prior to JSON parsing. Requests that exceed this limit are rejected with an error response. - -Use `--http-max-input-size` to configure the limit (default: `67108864` bytes / 64 MiB): - -```bash -python3 openai_frontend/main.py \ - --model-repository /path/to/models \ - --tokenizer meta-llama/Meta-Llama-3.1-8B-Instruct \ - --http-max-input-size 67108864 -``` - -The limit applies to all endpoints. Example error response: - -```json -{ - "error": { - "message": "Request content size exceeds the maximum allowed input size of 67108864 bytes. Use --http-max-input-size to increase the limit.", - "type": "invalid_request_error", - "code": "content_too_large" - } -} -``` diff --git a/python/openai/openai_frontend/engine/engine.py b/python/openai/openai_frontend/engine/engine.py index da68ac024b..9c90dec25e 100644 --- a/python/openai/openai_frontend/engine/engine.py +++ b/python/openai/openai_frontend/engine/engine.py @@ -1,4 +1,4 @@ -# Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -34,8 +34,6 @@ CreateChatCompletionResponse, CreateCompletionRequest, CreateCompletionResponse, - CreateEmbeddingRequest, - CreateEmbeddingResponse, Model, ) @@ -94,25 +92,3 @@ def completion( If request.stream is False, this returns a CreateCompletionResponse. """ pass - - def embedding(self, request: CreateEmbeddingRequest) -> CreateEmbeddingResponse: - """ - Returns a CreateEmbeddingResponse. - """ - pass - - async def load_model(self, model_name: str) -> Model: - """ - Loads a model by name. Only available in EXPLICIT model control mode. - Blocks until the model is fully loaded and ready, matching standard - Triton server load behavior. - """ - pass - - async def unload_model(self, model_name: str) -> None: - """ - Unloads a model by name. Only available in EXPLICIT model control mode. - Blocks until the model is fully unloaded, matching standard Triton - server unload behavior. In-flight requests complete before unload. - """ - pass diff --git a/python/openai/openai_frontend/engine/triton_engine.py b/python/openai/openai_frontend/engine/triton_engine.py index 19a5b2f157..e315cbbfd8 100644 --- a/python/openai/openai_frontend/engine/triton_engine.py +++ b/python/openai/openai_frontend/engine/triton_engine.py @@ -1,4 +1,4 @@ -# Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -27,74 +27,36 @@ from __future__ import annotations -import asyncio -import base64 -import json import time import uuid from dataclasses import dataclass -from typing import ( - Any, - AsyncIterable, - AsyncIterator, - Callable, - Dict, - List, - Literal, - Optional, - Tuple, - Union, -) +from typing import Any, AsyncIterable, AsyncIterator, Callable, Dict, List, Optional -import numpy as np import tritonserver from engine.engine import LLMEngine -from engine.utils.chat import load_chat_template, parse_chat_messages from engine.utils.tokenizer import get_tokenizer -from engine.utils.tool_call_parsers import ToolCallParser, ToolParserManager from engine.utils.triton import ( - RequestKind, - TritonLoraConfig, - _create_trtllm_embedding_request, - _create_trtllm_generate_request, - _create_vllm_embedding_request, - _create_vllm_generate_request, - _get_openai_chat_format_logprobs_from_vllm_response, - _get_openai_completion_format_logprobs_from_vllm_response, + _create_trtllm_inference_request, + _create_vllm_inference_request, _get_output, - _get_usage_from_response, - _parse_lora_configs, - _StreamingUsageAccumulator, _validate_triton_responses_non_streaming, ) from schemas.openai import ( ChatCompletionChoice, ChatCompletionFinishReason, - ChatCompletionLogprobs, - ChatCompletionMessageToolCall, - ChatCompletionMessageToolCallChunk, - ChatCompletionNamedToolChoice, ChatCompletionResponseMessage, ChatCompletionStreamingResponseChoice, ChatCompletionStreamResponseDelta, - ChatCompletionToolChoiceOption1, Choice, - CompletionUsage, CreateChatCompletionRequest, CreateChatCompletionResponse, CreateChatCompletionStreamResponse, CreateCompletionRequest, CreateCompletionResponse, - CreateEmbeddingRequest, - CreateEmbeddingResponse, - EmbeddingObject, FinishReason, - Function1, - Function2, Model, ObjectType, ) -from utils.utils import ClientError, ServerError # TODO: Improve type hints @@ -108,44 +70,26 @@ class TritonModelMetadata: model: tritonserver.Model # Tokenizers used for chat templates tokenizer: Optional[Any] - # LoRA names supported by the backend - lora_configs: Optional[List[TritonLoraConfig]] - # Name of the input tensor enabling "echo" parameter in /v1/completions endpoint - echo_tensor_name: Optional[str] # Time that model was loaded by Triton create_time: int # Conversion format between OpenAI and Triton requests - inference_request_converter: Callable - embedding_request_converter: Callable + request_converter: Callable class TritonLLMEngine(LLMEngine): def __init__( - self, - server: tritonserver.Server, - tokenizer: str, - default_max_tokens: int, - backend: Optional[str] = None, - lora_separator: Optional[str] = None, - tool_call_parser: Optional[str] = None, - chat_template: Optional[str] = None, + self, server: tritonserver.Server, tokenizer: str, backend: Optional[str] = None ): # Assume an already configured and started server self.server = server self.tokenizer = self._get_tokenizer(tokenizer) # TODO: Reconsider name of "backend" vs. something like "request_format" self.backend = backend - self.lora_separator = lora_separator - self.default_max_tokens = default_max_tokens + # NOTE: Creation time and model metadata will be static at startup for + # now, and won't account for dynamically loading/unloading models. + self.create_time = int(time.time()) self.model_metadata = self._get_model_metadata() - self._metadata_lock = asyncio.Lock() - self.tool_call_parser = ( - ToolParserManager.get_tool_parser_cls(tool_call_parser) - if tool_call_parser - else None - ) - self.chat_template = load_chat_template(chat_template) def ready(self) -> bool: return self.server.ready() @@ -156,64 +100,37 @@ def metrics(self) -> str: def models(self) -> List[Model]: models = [] for metadata in self.model_metadata.values(): - model_names = [metadata.name] - if ( - self.lora_separator is not None - and len(self.lora_separator) > 0 - and metadata.lora_configs is not None - ): - for lora_config in metadata.lora_configs: - model_names.append( - f"{metadata.name}{self.lora_separator}{lora_config.name}" - ) - - for model_name in model_names: - models.append( - Model( - id=model_name, - created=metadata.create_time, - object=ObjectType.model, - owned_by="Triton Inference Server", - ), - ) + models.append( + Model( + id=metadata.name, + created=metadata.create_time, + object=ObjectType.model, + owned_by="Triton Inference Server", + ), + ) return models async def chat( self, request: CreateChatCompletionRequest ) -> CreateChatCompletionResponse | AsyncIterator[str]: - model_name, lora_name = self._get_model_and_lora_name(request.model) - metadata = self.model_metadata.get(model_name) - self._validate_chat_request(request, metadata, lora_name) - - conversation = parse_chat_messages(request.messages) + metadata = self.model_metadata.get(request.model) + self._validate_chat_request(request, metadata) + conversation = [ + message.model_dump(exclude_none=True) for message in request.messages + ] add_generation_prompt = True - tool_dicts = ( - None - if request.tools is None - else [tool.model_dump() for tool in request.tools] - ) - prompt = metadata.tokenizer.apply_chat_template( conversation=conversation, tokenize=False, add_generation_prompt=add_generation_prompt, - tools=tool_dicts, - chat_template=self.chat_template, ) # Convert to Triton request format and perform inference responses = metadata.model.async_infer( - metadata.inference_request_converter( - metadata.model, - prompt, - request, - self._get_lora_config(model_name, lora_name), - metadata.echo_tensor_name, - self.default_max_tokens, - ) + metadata.request_converter(metadata.model, prompt, request) ) # Prepare and send responses back to client in OpenAI format @@ -224,19 +141,9 @@ async def chat( conversation, add_generation_prompt, default_role ) - tool_call_parser = ( - self.tool_call_parser(metadata.tokenizer) if self.tool_call_parser else None - ) - if request.stream: return self._streaming_chat_iterator( - request_id, - metadata.backend, - created, - request, - role, - tool_call_parser, - responses, + request_id, created, request.model, role, responses ) # Response validation with decoupled models in mind @@ -245,113 +152,34 @@ async def chat( response = responses[0] text = _get_output(response) - response_message, finish_reason = self._get_chat_completion_response_message( - request=request, - request_id=request_id, - tool_call_parser=tool_call_parser, - text=text, - role=role, - backend=metadata.backend, - ) - - usage = _get_usage_from_response( - response, metadata.backend, RequestKind.GENERATION - ) - - # Parse logprobs if requested - logprobs_data = None - if request.logprobs: - openai_logprobs = _get_openai_chat_format_logprobs_from_vllm_response( - response - ) - if openai_logprobs: - logprobs_data = ChatCompletionLogprobs(content=openai_logprobs) - return CreateChatCompletionResponse( id=request_id, choices=[ ChatCompletionChoice( index=0, - message=response_message, - logprobs=logprobs_data, - finish_reason=finish_reason, + message=ChatCompletionResponseMessage( + content=text, role=role, function_call=None + ), + logprobs=None, + finish_reason=ChatCompletionFinishReason.stop, ) ], created=created, model=request.model, system_fingerprint=None, object=ObjectType.chat_completion, - usage=usage, - ) - - def _get_chat_completion_response_message( - self, - request: CreateChatCompletionRequest, - request_id: str, - tool_call_parser: ToolCallParser, - text: str, - role: str, - backend: str, - ) -> Tuple[ChatCompletionResponseMessage, ChatCompletionFinishReason]: - response_message: ChatCompletionResponseMessage - auto_tools_called = False - tool_function_name = self._get_named_function_name(request=request) - if tool_function_name: - response_message = ChatCompletionResponseMessage( - content="", - role=role, - tool_calls=[ - ChatCompletionMessageToolCall( - id=request_id, - type="function", - function=Function1(name=tool_function_name, arguments=text), - ) - ], - ) - elif ( - tool_call_parser - and request.tools - and ( - request.tool_choice is None - or request.tool_choice.root == ChatCompletionToolChoiceOption1.auto - ) - ): - response_message = tool_call_parser.parse_tool_calls(text, role, backend) - auto_tools_called = ( - response_message.tool_calls is not None - and len(response_message.tool_calls.root) > 0 - ) - else: - response_message = ChatCompletionResponseMessage( - content=text, role=role, tool_calls=None - ) - - finish_reason = ( - ChatCompletionFinishReason.tool_calls - if auto_tools_called - else ChatCompletionFinishReason.stop ) - return response_message, finish_reason - async def completion( self, request: CreateCompletionRequest ) -> CreateCompletionResponse | AsyncIterator[str]: # Validate request and convert to Triton format - model_name, lora_name = self._get_model_and_lora_name(request.model) - metadata = self.model_metadata.get(model_name) - self._validate_completion_request(request, metadata, lora_name) + metadata = self.model_metadata.get(request.model) + self._validate_completion_request(request, metadata) # Convert to Triton request format and perform inference responses = metadata.model.async_infer( - metadata.inference_request_converter( - metadata.model, - request.prompt, - request, - self._get_lora_config(model_name, lora_name), - metadata.echo_tensor_name, - self.default_max_tokens, - ) + metadata.request_converter(metadata.model, request.prompt, request) ) # Prepare and send responses back to client in OpenAI format @@ -359,7 +187,7 @@ async def completion( created = int(time.time()) if request.stream: return self._streaming_completion_iterator( - request_id, created, request, responses, metadata.backend + request_id, created, metadata.name, responses ) # Response validation with decoupled models in mind @@ -368,21 +196,10 @@ async def completion( response = responses[0] text = _get_output(response) - usage = _get_usage_from_response( - response, metadata.backend, RequestKind.GENERATION - ) - - # Parse logprobs if requested - logprobs_data = None - if request.logprobs is not None and request.logprobs > 0: - logprobs_data = _get_openai_completion_format_logprobs_from_vllm_response( - response - ) - choice = Choice( finish_reason=FinishReason.stop, index=0, - logprobs=logprobs_data, + logprobs=None, text=text, ) return CreateCompletionResponse( @@ -391,61 +208,9 @@ async def completion( system_fingerprint=None, object=ObjectType.text_completion, created=created, - model=request.model, - usage=usage, + model=metadata.name, ) - async def embedding( - self, request: CreateEmbeddingRequest - ) -> CreateEmbeddingResponse: - # Validate request and convert to Triton format - model_name, _ = self._get_model_and_lora_name(request.model) - metadata = self.model_metadata.get(model_name) - self._validate_embedding_request(request, metadata) - - # Convert to Triton request format and perform inference - responses = metadata.model.async_infer( - metadata.embedding_request_converter( - metadata.model, - request, - ) - ) - - # Response validation with decoupled models in mind - responses = [response async for response in responses] - _validate_triton_responses_non_streaming(responses) - response = responses[0] - - # Extract embedding from response (currently stored as JSON string in text_output) - embedding_json = _get_output(response) - embedding_list = json.loads(embedding_json) - - usage = _get_usage_from_response( - response, metadata.backend, RequestKind.EMBEDDING - ) - - embedding = self._get_embedding(embedding_list, request.encoding_format) - embedding_obj = EmbeddingObject( - embedding=embedding, index=0, object="embedding" - ) - - return CreateEmbeddingResponse( - object="list", - data=[embedding_obj], - model=request.model, - usage=usage, - ) - - @staticmethod - def _get_embedding( - embedding: List[float], encoding_format: Literal["float", "base64"] - ) -> Union[list[float], str]: - if encoding_format == "float": - return embedding - else: - embedding_bytes = np.array(embedding, dtype="float32").tobytes() - return base64.b64encode(embedding_bytes).decode("utf-8") - # TODO: This behavior should be tested further def _get_first_response_role( self, conversation: List[Dict], add_generation_prompt: bool, default_role: str @@ -456,34 +221,18 @@ def _get_first_response_role( return conversation[-1]["role"] # TODO: Expose explicit flag to catch edge cases - def _determine_request_converter(self, backend: str, request_type: RequestKind): + def _determine_request_converter(self, backend: str): # Allow manual override of backend request format if provided by user if self.backend: backend = self.backend # Request conversion from OpenAI format to backend-specific format if backend == "vllm": - if request_type == RequestKind.GENERATION: - return _create_vllm_generate_request - else: - return _create_vllm_embedding_request + return _create_vllm_inference_request # Use TRT-LLM format as default for everything else. This could be # an ensemble, a python or BLS model, a TRT-LLM backend model, etc. - if request_type == RequestKind.GENERATION: - return _create_trtllm_generate_request - else: - return _create_trtllm_embedding_request - - def _get_model_and_lora_name(self, request_model_name: str): - if self.lora_separator is None or len(self.lora_separator) == 0: - return request_model_name, None - - names = request_model_name.split(self.lora_separator) - if len(names) != 2: - return request_model_name, None - - return names[0], names[1] + return _create_trtllm_inference_request def _get_tokenizer(self, tokenizer_name: str): tokenizer = None @@ -492,114 +241,30 @@ def _get_tokenizer(self, tokenizer_name: str): return tokenizer - def _build_model_metadata(self, name: str) -> TritonModelMetadata: - model = self.server.model(name) - backend = model.config()["backend"] - if not backend and model.config()["platform"] == "ensemble": - backend = "ensemble" - print(f"Found model: {name=}, {backend=}") - - lora_configs = _parse_lora_configs( - self.server.options.model_repository, - name, - model.version, - backend if self.backend is None else self.backend, - ) - - echo_tensor_name = None - for input in model.config()["input"]: - if input["name"] in [ - "exclude_input_in_output", - "sampling_param_exclude_input_from_output", - ]: - echo_tensor_name = input["name"] - break - - return TritonModelMetadata( - name=name, - backend=backend, - model=model, - tokenizer=self.tokenizer, - lora_configs=lora_configs, - echo_tensor_name=echo_tensor_name, - create_time=int(time.time()), - inference_request_converter=self._determine_request_converter( - backend, RequestKind.GENERATION - ), - embedding_request_converter=self._determine_request_converter( - backend, RequestKind.EMBEDDING - ), - ) - def _get_model_metadata(self) -> Dict[str, TritonModelMetadata]: - # One tokenizer is shared for all loaded models; creation time is per model. + # One tokenizer and creation time shared for all loaded models for now. model_metadata = {} - for name, _ in self.server.models(exclude_not_ready=True).keys(): - model_metadata[name] = self._build_model_metadata(name) - return model_metadata - - async def load_model(self, model_name: str) -> Model: - if ( - self.server.options.model_control_mode - != tritonserver.ModelControlMode.EXPLICIT - ): - raise ClientError( - "Model load/unload requires --model-control-mode=explicit" - ) - - async with self._metadata_lock: - if model_name in self.model_metadata: - raise ClientError(f"Model '{model_name}' is already loaded") - - # Blocking C API call dispatched to thread pool to avoid blocking - # the event loop. The C API blocks until model is fully loaded and - # ready, matching standard Triton server behavior. - try: - metadata = await asyncio.to_thread(self._load_model_sync, model_name) - except tritonserver.InvalidArgumentError as e: - raise ClientError(f"Failed to load model '{model_name}': {e}") - except tritonserver.TritonError as e: - raise ServerError(f"Failed to load model '{model_name}': {e}") - - self.model_metadata[model_name] = metadata - return Model( - id=model_name, - created=metadata.create_time, - object=ObjectType.model, - owned_by="Triton Inference Server", - ) - - def _load_model_sync(self, model_name: str) -> TritonModelMetadata: - self.server.load(model_name) - return self._build_model_metadata(model_name) - - async def unload_model(self, model_name: str) -> None: - if ( - self.server.options.model_control_mode - != tritonserver.ModelControlMode.EXPLICIT - ): - raise ClientError( - "Model load/unload requires --model-control-mode=explicit" + # Read all triton models and store the necessary metadata for each + for name, _ in self.server.models().keys(): + model = self.server.model(name) + backend = model.config()["backend"] + # Explicitly handle ensembles to avoid any runtime validation errors + if not backend and model.config()["platform"] == "ensemble": + backend = "ensemble" + print(f"Found model: {name=}, {backend=}") + + metadata = TritonModelMetadata( + name=name, + backend=backend, + model=model, + tokenizer=self.tokenizer, + create_time=self.create_time, + request_converter=self._determine_request_converter(backend), ) + model_metadata[name] = metadata - async with self._metadata_lock: - if model_name not in self.model_metadata: - raise ClientError(f"Unknown model: {model_name}") - - # Blocking C API call dispatched to thread pool. The C API handles - # in-flight request draining and conflict resolution internally. - try: - await asyncio.to_thread(self._unload_model_sync, model_name) - except tritonserver.InvalidArgumentError as e: - raise ClientError(f"Failed to unload model '{model_name}': {e}") - except tritonserver.TritonError as e: - raise ServerError(f"Failed to unload model '{model_name}': {e}") - - del self.model_metadata[model_name] - - def _unload_model_sync(self, model_name: str) -> None: - self.server.unload(model_name) + return model_metadata def _get_streaming_chat_response_chunk( self, @@ -607,7 +272,6 @@ def _get_streaming_chat_response_chunk( request_id: str, created: int, model: str, - usage: Optional[CompletionUsage] = None, ) -> CreateChatCompletionStreamResponse: return CreateChatCompletionStreamResponse( id=request_id, @@ -616,7 +280,6 @@ def _get_streaming_chat_response_chunk( model=model, system_fingerprint=None, object=ObjectType.chat_completion_chunk, - usage=usage, ) def _get_first_streaming_chat_response( @@ -632,217 +295,55 @@ def _get_first_streaming_chat_response( finish_reason=None, ) chunk = self._get_streaming_chat_response_chunk( - choice, request_id, created, model, usage=None + choice, request_id, created, model + ) + return chunk + + def _get_nth_streaming_chat_response( + self, + request_id: str, + created: int, + model: str, + response: tritonserver.InferenceResponse, + ) -> CreateChatCompletionStreamResponse: + text = _get_output(response) + choice = ChatCompletionStreamingResponseChoice( + index=0, + delta=ChatCompletionStreamResponseDelta( + role=None, content=text, function_call=None + ), + logprobs=None, + finish_reason=ChatCompletionFinishReason.stop if response.final else None, + ) + + chunk = self._get_streaming_chat_response_chunk( + choice, request_id, created, model ) return chunk async def _streaming_chat_iterator( self, request_id: str, - backend: str, created: int, - request: CreateChatCompletionRequest, + model: str, role: str, - tool_call_parser: ToolCallParser, responses: AsyncIterable, ) -> AsyncIterator[str]: - model = request.model - - tool_function_name = self._get_named_function_name(request=request) - - # Determine whether tools are in use with "auto" tool choice - tool_choice_auto = ( - tool_call_parser - and not tool_function_name - and self._should_stream_with_auto_tool_parsing(request) - ) - - previous_text = "" - include_usage = request.stream_options and request.stream_options.include_usage - usage_accumulator = _StreamingUsageAccumulator(backend) - chunk = self._get_first_streaming_chat_response( request_id, created, model, role ) yield f"data: {chunk.model_dump_json(exclude_unset=True)}\n\n" async for response in responses: - delta_text = _get_output(response) - if include_usage: - usage_accumulator.update(response) - - ( - response_delta, - finish_reason, - current_text, - ) = self._get_streaming_response_delta( - previous_text=previous_text, - delta_text=delta_text, - tool_function_name=tool_function_name, - tool_choice_auto=tool_choice_auto, - tool_call_parser=tool_call_parser, - backend=backend, - is_final_response=response.final, - ) - previous_text = current_text - - # Parse logprobs for this chunk if requested - chunk_logprobs = None - if request.logprobs: - openai_logprobs = _get_openai_chat_format_logprobs_from_vllm_response( - response - ) - if openai_logprobs: - chunk_logprobs = ChatCompletionLogprobs(content=openai_logprobs) - - # if the response delta is None (e.g. because it was a - # "control token" for tool calls or the parser otherwise - # wasn't ready to send a token, then - # get the next token without streaming a chunk - if response_delta is None and finish_reason is None: - continue - - if finish_reason and response_delta is None: - response_delta = ChatCompletionStreamResponseDelta(content="") - - choice = ChatCompletionStreamingResponseChoice( - index=0, - delta=response_delta, - logprobs=chunk_logprobs, - finish_reason=finish_reason, - ) - - chunk = self._get_streaming_chat_response_chunk( - choice, request_id, created, model, usage=None + chunk = self._get_nth_streaming_chat_response( + request_id, created, model, response ) yield f"data: {chunk.model_dump_json(exclude_unset=True)}\n\n" - # Send the final usage chunk if requested via stream_options. - if include_usage: - usage_payload = usage_accumulator.get_final_usage() - if usage_payload: - final_usage_chunk = CreateChatCompletionStreamResponse( - id=request_id, - choices=[], - created=created, - model=model, - system_fingerprint=None, - object=ObjectType.chat_completion_chunk, - usage=usage_payload, - ) - yield f"data: {final_usage_chunk.model_dump_json(exclude_unset=True)}\n\n" - yield "data: [DONE]\n\n" - def _get_streaming_response_delta( - self, - previous_text: str, - delta_text: str, - tool_function_name: Optional[str], - tool_choice_auto: bool, - tool_call_parser: ToolCallParser, - backend: str, - is_final_response: bool, - ) -> Tuple[ - Optional[ChatCompletionStreamResponseDelta], - Optional[ChatCompletionFinishReason], - str, - ]: - response_delta: Optional[ChatCompletionStreamResponseDelta] - current_text = "" - if tool_function_name: - response_delta = ChatCompletionStreamResponseDelta( - tool_calls=[ - ChatCompletionMessageToolCallChunk( - index=0, - function=Function2( - name=tool_function_name, arguments=delta_text - ), - ) - ] - ) - elif tool_choice_auto: - current_text = previous_text + delta_text - response_delta = tool_call_parser.parse_tool_calls_streaming( - current_text=current_text, delta_text=delta_text, backend=backend - ) - else: - response_delta = ChatCompletionStreamResponseDelta( - role=None, content=delta_text, function_call=None - ) - - if is_final_response: - auto_tools_called = False - if tool_call_parser: - auto_tools_called = len(tool_call_parser.prev_tool_call_arr) > 0 - index = ( - len(tool_call_parser.prev_tool_call_arr) - 1 - if auto_tools_called - else 0 - ) - else: - index = 0 - - # check to make sure we haven't "forgotten" to stream - # any tokens that were generated but previously - # matched by partial json parsing, such as '}'. - # only happens if we are NOT using structured outputs - # or guided decoding - if ( - self._should_check_for_unstreamed_tool_arg_tokens( - response_delta=response_delta, - auto_tools_called=auto_tools_called, - ) - and tool_call_parser - ): - latest_delta_len = 0 - if ( - isinstance(response_delta.tool_calls[0].function, Function2) - ) and isinstance(response_delta.tool_calls[0].function.arguments, str): - latest_delta_len = len( - response_delta.tool_calls[0].function.arguments - ) - # get the expected call based on partial JSON - # parsing which "autocompletes" the JSON - expected_call = json.dumps( - tool_call_parser.prev_tool_call_arr[index].get("arguments", {}), - ensure_ascii=False, - ) - # get what we've streamed so far for arguments - # for the current tool - actual_call = tool_call_parser.streamed_args_for_tool[index] - if latest_delta_len > 0: - actual_call = actual_call[:-latest_delta_len] - - # check to see if there's anything left to stream - remaining_call = expected_call.replace(actual_call, "", 1) - - response_delta = ChatCompletionStreamResponseDelta( - tool_calls=[ - ChatCompletionMessageToolCallChunk( - index=index, - function=Function2(arguments=remaining_call).model_dump( - exclude_none=True - ), - ) - ] - ) - - finish_reason = ( - ChatCompletionFinishReason.tool_calls - if auto_tools_called - else ChatCompletionFinishReason.stop - ) - else: - finish_reason = None - - return response_delta, finish_reason, current_text - def _validate_chat_request( - self, - request: CreateChatCompletionRequest, - metadata: TritonModelMetadata, - lora_name: str | None, + self, request: CreateChatCompletionRequest, metadata: TritonModelMetadata ): """ Validates a chat request to align with currently supported features. @@ -850,131 +351,35 @@ def _validate_chat_request( # Reject missing internal information needed to do inference if not metadata: - raise ClientError(f"Unknown model: {request.model}") + raise Exception(f"Unknown model: {request.model}") if not metadata.tokenizer: - raise ServerError("Unknown tokenizer") + raise Exception("Unknown tokenizer") if not metadata.backend: - raise ServerError("Unknown backend") + raise Exception("Unknown backend") - if not metadata.inference_request_converter: - raise ServerError( - f"Unknown inference request format for model: {request.model}" - ) - - if not metadata.embedding_request_converter: - raise ServerError( - f"Unknown embedding request format for model: {request.model}" - ) - - if ( - metadata.lora_configs is not None - and lora_name is not None - and lora_name - not in [lora_config.name for lora_config in metadata.lora_configs] - ): - raise ClientError(f"Unknown LoRA: {lora_name}; for model: {request.model}") + if not metadata.request_converter: + raise Exception(f"Unknown request format for model: {request.model}") # Reject unsupported features if requested if request.n and request.n > 1: - raise ClientError( + raise Exception( f"Received n={request.n}, but only single choice (n=1) is currently supported" ) - if request.logit_bias is not None: - raise ClientError("logit bias is not currently supported") - - # Logprobs are only supported for vLLM backend currently - if metadata.backend != "vllm" and ( - request.logprobs or request.top_logprobs is not None - ): - raise ClientError( - "logprobs are currently available only for the vLLM backend" - ) - - if request.top_logprobs is not None and not request.logprobs: - raise ClientError("`top_logprobs` can only be used when `logprobs` is True") - - self._verify_chat_tool_call_settings(request=request) - - if request.stream_options and not request.stream: - raise ClientError("`stream_options` can only be used when `stream` is True") - - def _verify_chat_tool_call_settings(self, request: CreateChatCompletionRequest): - if ( - request.tool_choice - and request.tool_choice.root == ChatCompletionToolChoiceOption1.required - and not request.tools - ): - raise ClientError( - '"required" tool choice requires CreateChatCompletionRequest.tools to be provided' - ) - - if ( - request.tool_choice - and isinstance(request.tool_choice.root, ChatCompletionNamedToolChoice) - and not request.tools - ): - raise ClientError( - "Named tool choice requires CreateChatCompletionRequest.tools to be provided" - ) - - if ( - request.tool_choice - and request.tool_choice.root == ChatCompletionToolChoiceOption1.auto - and self.tool_call_parser is None - ): - raise ClientError( - '"auto" tool choice requires --tool-call-parser to be set' - ) - - if ( - request.tool_choice is None - and request.tools - and self.tool_call_parser is None - ): - raise ClientError( - "having tools in the request requires --tool-call-parser to be set" - ) + if request.logit_bias is not None or request.logprobs: + raise Exception("logit bias and log probs not currently supported") async def _streaming_completion_iterator( - self, - request_id: str, - created: int, - request: CreateCompletionRequest, - responses: AsyncIterable, - backend: str, + self, request_id: str, created: int, model: str, responses: AsyncIterable ) -> AsyncIterator[str]: - model = request.model - include_usage = request.stream_options and request.stream_options.include_usage - usage_accumulator = _StreamingUsageAccumulator(backend) - current_offset = 0 - async for response in responses: - if include_usage: - usage_accumulator.update(response) - text = _get_output(response) - - # Parse logprobs for this chunk if requested - chunk_logprobs = None - if request.logprobs is not None and request.logprobs > 0: - chunk_logprobs = ( - _get_openai_completion_format_logprobs_from_vllm_response(response) - ) - # Adjust text offsets based on accumulated output - if chunk_logprobs and chunk_logprobs.text_offset: - chunk_logprobs.text_offset = [ - offset + current_offset for offset in chunk_logprobs.text_offset - ] - - current_offset += len(text) - choice = Choice( finish_reason=FinishReason.stop if response.final else None, index=0, - logprobs=chunk_logprobs, + logprobs=None, text=text, ) chunk = CreateCompletionResponse( @@ -984,180 +389,48 @@ async def _streaming_completion_iterator( object=ObjectType.text_completion, created=created, model=model, - usage=None, ) yield f"data: {chunk.model_dump_json(exclude_unset=True)}\n\n" - # Send the final usage chunk if requested via stream_options. - if include_usage: - usage_payload = usage_accumulator.get_final_usage() - if usage_payload: - final_usage_chunk = CreateCompletionResponse( - id=request_id, - choices=[], - system_fingerprint=None, - object=ObjectType.text_completion, - created=created, - model=model, - usage=usage_payload, - ) - yield f"data: {final_usage_chunk.model_dump_json(exclude_unset=True)}\n\n" - yield "data: [DONE]\n\n" def _validate_completion_request( - self, - request: CreateCompletionRequest, - metadata: TritonModelMetadata, - lora_name: str | None, + self, request: CreateCompletionRequest, metadata: TritonModelMetadata ): """ Validates a completions request to align with currently supported features. """ # Reject missing internal information needed to do inference if not metadata: - raise ClientError(f"Unknown model: {request.model}") + raise Exception(f"Unknown model: {request.model}") if not metadata.backend: - raise ServerError("Unknown backend") - - if not metadata.inference_request_converter: - raise ServerError( - f"Unknown inference request format for model: {request.model}" - ) - - if not metadata.embedding_request_converter: - raise ServerError( - f"Unknown embedding request format for model: {request.model}" - ) + raise Exception("Unknown backend") - if ( - metadata.lora_configs is not None - and lora_name is not None - and lora_name - not in [lora_config.name for lora_config in metadata.lora_configs] - ): - raise ClientError(f"Unknown LoRA: {lora_name}; for model: {request.model}") + if not metadata.request_converter: + raise Exception(f"Unknown request format for model: {request.model}") # Reject unsupported features if requested if request.suffix is not None: - raise ClientError("suffix is not currently supported") + raise Exception("suffix is not currently supported") if not request.prompt: - raise ClientError("prompt must be non-empty") + raise Exception("prompt must be non-empty") # Currently only support single string as input if not isinstance(request.prompt, str): - raise ClientError("only single string input is supported") - - if "best_of" in request.model_fields_set and metadata.backend == "vllm": - raise ClientError( - "best_of is no longer supported in vLLM backend, removed from vLLM V1 engine" - ) + raise Exception("only single string input is supported") if request.n and request.n > 1: - raise ClientError( + raise Exception( f"Received n={request.n}, but only single choice (n=1) is currently supported" ) if request.best_of and request.best_of > 1: - raise ClientError( + raise Exception( f"Received best_of={request.best_of}, but only single choice (best_of=1) is currently supported" ) - if request.logit_bias is not None: - raise ClientError("logit bias is not supported") - - # Logprobs are only supported for vLLM backend currently - if ( - request.logprobs is not None - and request.logprobs > 0 - and metadata.backend != "vllm" - ): - raise ClientError( - "logprobs are currently available only for the vLLM backend" - ) - - if request.stream_options and not request.stream: - raise ClientError("`stream_options` can only be used when `stream` is True") - - def _validate_embedding_request( - self, - request: CreateEmbeddingRequest, - metadata: TritonModelMetadata, - ): - """ - Validates an embedding request to align with currently supported features. - """ - - # Reject missing internal information needed to do inference - if not metadata: - raise ClientError(f"Unknown model: {request.model}") - - if not metadata.backend: - raise ServerError("Unknown backend") - - if not metadata.inference_request_converter: - raise ServerError( - f"Unknown inference request format for model: {request.model}" - ) - - if not metadata.embedding_request_converter: - raise ServerError( - f"Unknown embedding request format for model: {request.model}" - ) - - def _should_stream_with_auto_tool_parsing( - self, request: CreateChatCompletionRequest - ): - has_tools = request.tools and self.tool_call_parser - auto_tool = ( - request.tool_choice is None - or request.tool_choice.root == ChatCompletionToolChoiceOption1.auto - ) - return has_tools and auto_tool - - def _should_check_for_unstreamed_tool_arg_tokens( - self, response_delta: ChatCompletionStreamResponseDelta, auto_tools_called - ): - return bool( - auto_tools_called - and self.tool_call_parser - and response_delta - and response_delta.tool_calls - and response_delta.tool_calls[0] - and response_delta.tool_calls[0].function - and response_delta.tool_calls[0].function.arguments is not None - ) - - def _get_named_function_name( - self, request: CreateChatCompletionRequest - ) -> Optional[str]: - if request.tool_choice and isinstance( - request.tool_choice.root, ChatCompletionNamedToolChoice - ): - tool_choice_function_name = request.tool_choice.root.function.name - else: - tool_choice_function_name = None - - if ( - request.tool_choice - and request.tool_choice.root == ChatCompletionToolChoiceOption1.required - ): - tool_choice_required_function_name = request.tools[0].function.name - else: - tool_choice_required_function_name = None - - return tool_choice_function_name or tool_choice_required_function_name - - def _get_lora_config( - self, model_name: str, lora_name: Optional[str] - ) -> TritonLoraConfig: - model_metadata = self.model_metadata.get(model_name) - if lora_name is None or model_metadata.lora_configs is None: - return None - for lora_config in model_metadata.lora_configs: - if lora_config.name == lora_name: - return lora_config - raise ClientError(f"Unknown LoRA: {lora_name}; for model: {model_name}") + if request.logit_bias is not None or request.logprobs is not None: + raise Exception("logit bias and log probs not supported") diff --git a/python/openai/openai_frontend/engine/utils/chat.py b/python/openai/openai_frontend/engine/utils/chat.py deleted file mode 100644 index 2c941e666c..0000000000 --- a/python/openai/openai_frontend/engine/utils/chat.py +++ /dev/null @@ -1,168 +0,0 @@ -# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import json -from typing import Dict, Iterable, List, Optional, Required, TypedDict, Union, cast - -# FIXME: Converge on single set of types in either schemas.openai or openai.types -from openai.types.chat import ChatCompletionMessageToolCallParam -from openai.types.chat.chat_completion_message_tool_call_param import Function -from schemas.openai import ( - ChatCompletionMessageToolCall, - ChatCompletionRequestAssistantMessage, - ChatCompletionRequestMessage, - ChatCompletionRequestMessageContentPart, - ChatCompletionRequestToolMessage, - ChatCompletionRequestUserMessage, - Type1, -) -from utils.utils import ClientError - - -class ConversationMessage(TypedDict, total=False): - role: Required[str] - """The role of the message's author.""" - - content: Union[Optional[str], List[Dict[str, str]]] - """The contents of the message""" - - tool_call_id: Optional[str] - """Tool call that this message is responding to.""" - - name: Optional[str] - """The name of the function to call""" - - tool_calls: Optional[Iterable[ChatCompletionMessageToolCallParam]] - """The tool calls generated by the model, such as function calls.""" - - -def _frontend_schema_to_openai_schema_completion_tool_call( - tool_call_param: ChatCompletionMessageToolCall, -) -> ChatCompletionMessageToolCallParam: - return ChatCompletionMessageToolCallParam( - id=tool_call_param.id, - type=tool_call_param.type, - function=Function( - name=tool_call_param.function.name, - arguments=tool_call_param.function.arguments, - ), - ) - - -def _parse_chat_message_content_parts( - role: str, parts: List[ChatCompletionRequestMessageContentPart] -) -> ConversationMessage: - content = list[Dict]() - - for part in parts: - if part.root.type == Type1.text or part.root.type == "text": - parse_res = {"type": "text", "text": part.root.text} - content.append(parse_res) - else: - raise ClientError( - f"only text message is supported, but got {part.root.type}" - ) - - return ConversationMessage(role=role, content=content) - - -def _parse_chat_message_content( - message: ChatCompletionRequestMessage, -) -> ConversationMessage: - role = message.root.role - content = message.root.content - - if content is None or isinstance(content, str): - result_msg = ConversationMessage(role=role, content=content) - else: # content is a list of message parts - result_msg = _parse_chat_message_content_parts( - role, - content, - ) - - if role == "assistant": - parsed_msg = cast(ChatCompletionRequestAssistantMessage, message.root) - - if parsed_msg.tool_calls: - result_msg["tool_calls"] = list( - [ - _frontend_schema_to_openai_schema_completion_tool_call(tool_call) - for tool_call in parsed_msg.tool_calls.root - ] - ) - elif role == "tool": - parsed_msg = cast(ChatCompletionRequestToolMessage, message.root) - if parsed_msg.tool_call_id: - result_msg["tool_call_id"] = parsed_msg.tool_call_id - - if isinstance(message.root, ChatCompletionRequestUserMessage) and isinstance( - message.root.name, str - ): - result_msg["name"] = message.root.name - - return result_msg - - -def _postprocess_messages(messages: List[ConversationMessage]) -> None: - # per the Transformers docs & maintainers, tool call arguments in - # assistant-role messages with tool_calls need to be dicts not JSON str - - # this is how tool-use chat templates will expect them moving forwards - # so, for messages that have tool_calls, parse the string (which we get - # from openAI format) to dict - for message in messages: - if ( - message["role"] == "assistant" - and "tool_calls" in message - and isinstance(message["tool_calls"], list) - ): - for item in message["tool_calls"]: - item["function"]["arguments"] = json.loads( - item["function"]["arguments"] - ) - - -def parse_chat_messages( - messages: List[ChatCompletionRequestMessage], -) -> List[ConversationMessage]: - conversation: List[ConversationMessage] = [] - - for msg in messages: - sub_message = _parse_chat_message_content(msg) - conversation.append(sub_message) - - _postprocess_messages(conversation) - - return conversation - - -# This function loads the chat template file content -# if the user chooses to use a chat template different from -# the original one provided with the model's tokenizer. -def load_chat_template(chat_template) -> Optional[str]: - if chat_template is None: - return None - - with open(chat_template) as f: - return f.read() diff --git a/python/openai/openai_frontend/engine/utils/tokenizer.py b/python/openai/openai_frontend/engine/utils/tokenizer.py index 73184e843f..982e553cea 100644 --- a/python/openai/openai_frontend/engine/utils/tokenizer.py +++ b/python/openai/openai_frontend/engine/utils/tokenizer.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2026, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -32,8 +32,6 @@ from transformers import AutoTokenizer, PreTrainedTokenizer, PreTrainedTokenizerFast -AnyTokenizer = Union[PreTrainedTokenizer, PreTrainedTokenizerFast] - def get_cached_tokenizer( tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] @@ -47,6 +45,7 @@ def get_cached_tokenizer( function caches these properties for faster access.""" tokenizer_all_special_ids = set(tokenizer.all_special_ids) + tokenizer_all_special_tokens_extended = tokenizer.all_special_tokens_extended tokenizer_all_special_tokens = set(tokenizer.all_special_tokens) tokenizer_len = len(tokenizer) @@ -59,6 +58,10 @@ def all_special_ids(self): def all_special_tokens(self): return tokenizer_all_special_tokens + @property + def all_special_tokens_extended(self): + return tokenizer_all_special_tokens_extended + def __len__(self): return tokenizer_len diff --git a/python/openai/openai_frontend/engine/utils/tool_call_parsers/__init__.py b/python/openai/openai_frontend/engine/utils/tool_call_parsers/__init__.py deleted file mode 100644 index d4bec1e62f..0000000000 --- a/python/openai/openai_frontend/engine/utils/tool_call_parsers/__init__.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -# Adapted from -# https://github.com/vllm-project/vllm/blob/main/vllm/entrypoints/openai/tool_parsers/__init__.py -# Copyright 2024 The vLLM team. - -from .llama_tool_call_parser import Llama3JsonToolParser -from .mistral_tool_call_parser import MistralToolParser -from .tool_call_parser import ToolCallParser, ToolParserManager - -__all__ = [ - "ToolCallParser", - "ToolParserManager", - "Llama3JsonToolParser", - "MistralToolParser", -] diff --git a/python/openai/openai_frontend/engine/utils/tool_call_parsers/llama_tool_call_parser.py b/python/openai/openai_frontend/engine/utils/tool_call_parsers/llama_tool_call_parser.py deleted file mode 100644 index ba63189707..0000000000 --- a/python/openai/openai_frontend/engine/utils/tool_call_parsers/llama_tool_call_parser.py +++ /dev/null @@ -1,289 +0,0 @@ -# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -# Adapted from -# https://github.com/vllm-project/vllm/blob/main/vllm/entrypoints/openai/tool_parsers/llama_tool_parser.py -# Copyright 2024 The vLLM team. -import json -import uuid -from typing import Union - -import partial_json_parser -from engine.utils.tokenizer import AnyTokenizer -from engine.utils.tool_call_parsers.tool_call_parser import ( - ToolCallParser, - ToolParserManager, -) -from partial_json_parser.core.options import Allow -from schemas.openai import ( - ChatCompletionMessageToolCall, - ChatCompletionMessageToolCallChunk, - ChatCompletionMessageToolCalls, - ChatCompletionResponseMessage, - ChatCompletionStreamResponseDelta, - Function1, - Function2, -) - -from .utils import find_common_prefix, is_complete_json, partial_json_loads - - -@ToolParserManager.register_module("llama3") -class Llama3JsonToolParser(ToolCallParser): - def __init__(self, tokenizer: AnyTokenizer): - super().__init__(tokenizer) - - # initialize properties used for state when parsing tool calls in - # streaming mode - self.prev_tool_call_arr: list[dict] = [] - self.current_tool_id: int = -1 - self.current_tool_name_sent: bool = False - self.streamed_args_for_tool: list[ - str - ] = [] # map what has been streamed for each tool so far to a list - - self.bot_token = "<|python_tag|>" - - def parse_tool_calls( - self, full_text: str, role: str, backend: str - ) -> ChatCompletionResponseMessage: - """ - Extract the tool calls from a complete model response. - """ - # case -- if a tool call token is not present, return a text response - if not (full_text.startswith(self.bot_token) or full_text.startswith("{")): - return ChatCompletionResponseMessage( - tool_calls=None, content=full_text, role=role - ) - - original_full_text = full_text - try: - # FIXME: tensorrt_llm backend might generate some unnecessary text messages - # after the tool call json text starting with "assistant\n\n". - if backend != "vllm": - last_index = full_text.find("assistant\n\n") - if last_index > 0: - full_text = full_text[:last_index] - - # load the JSON, and then use it to build the Function and - # Tool Call - dec = json.JSONDecoder() - function_call_arr = [] - - # depending on the prompt format the Llama model may or may not - # prefix the output with the <|python_tag|> token - start_idx = ( - len(self.bot_token) if full_text.startswith(self.bot_token) else 0 - ) - while start_idx < len(full_text): - (obj, end_idx) = dec.raw_decode(full_text[start_idx:]) - start_idx += end_idx + len("; ") - function_call_arr.append(obj) - - tool_calls = ChatCompletionMessageToolCalls( - root=[ - ChatCompletionMessageToolCall( - id=f"cmpl-{uuid.uuid1()}", - type="function", - function=Function1( - name=raw_function_call["name"], - # function call args are JSON but as a string - arguments=json.dumps( - raw_function_call["arguments"] - if "arguments" in raw_function_call - else raw_function_call["parameters"] - ), - ), - ) - for raw_function_call in function_call_arr - ] - ) - - # get any content before the tool call - ret = ChatCompletionResponseMessage( - tool_calls=tool_calls, content="", role=role - ) - return ret - - except Exception as e: - # return information to just treat the tool call as regular JSON - return ChatCompletionResponseMessage( - tool_calls=None, content=original_full_text, role=role - ) - - def parse_tool_calls_streaming( - self, current_text: str, delta_text: str, backend: str - ) -> Union[ChatCompletionStreamResponseDelta, None]: - if not ( - current_text.startswith(self.bot_token) or current_text.startswith("{") - ): - return ChatCompletionStreamResponseDelta(content=delta_text) - - # bit mask flags for partial JSON parsing. If the name hasn't been - # sent yet, don't allow sending - # an incomplete string since OpenAI only ever (as far as I have - # seen) allows sending the entire tool/ function name at once. - flags = Allow.ALL if self.current_tool_name_sent else Allow.ALL & ~Allow.STR - try: - tool_call_arr = [] - is_complete = [] - try: - # depending on the prompt format the Llama model may or may not - # prefix the output with the <|python_tag|> token - start_idx = ( - len(self.bot_token) - if current_text.startswith(self.bot_token) - else 0 - ) - while start_idx < len(current_text): - (obj, end_idx) = partial_json_loads(current_text[start_idx:], flags) - is_complete.append( - is_complete_json(current_text[start_idx : start_idx + end_idx]) - ) - start_idx += end_idx + len("; ") - # depending on the prompt Llama can use - # either arguments or parameters - if "parameters" in obj: - assert ( - "arguments" not in obj - ), "model generated both parameters and arguments" - obj["arguments"] = obj["parameters"] - tool_call_arr.append(obj) - except partial_json_parser.core.exceptions.MalformedJSON: - return None - - # select as the current tool call the one we're on the state at - current_tool_call: dict = ( - tool_call_arr[self.current_tool_id] if len(tool_call_arr) > 0 else {} - ) - - # case -- if no tokens have been streamed for the tool, e.g. - # only the array brackets, stream nothing - if len(tool_call_arr) == 0: - return None - - # case: we are starting a new tool in the array - # -> array has > 0 length AND length has moved past cursor - elif ( - len(tool_call_arr) > 0 and len(tool_call_arr) > self.current_tool_id + 1 - ): - # if we're moving on to a new call, first make sure we - # haven't missed anything in the previous one that was - # auto-generated due to JSON completions, but wasn't - # streamed to the client yet. - if self.current_tool_id >= 0: - cur_arguments = current_tool_call.get("arguments") - if cur_arguments: - cur_args_json = json.dumps(cur_arguments) - sent = len(self.streamed_args_for_tool[self.current_tool_id]) - argument_diff = cur_args_json[sent:] - - delta = ChatCompletionStreamResponseDelta( - tool_calls=[ - ChatCompletionMessageToolCallChunk( - index=self.current_tool_id, - function=Function2( - arguments=argument_diff - ).model_dump(exclude_none=True), - ) - ] - ) - self.streamed_args_for_tool[ - self.current_tool_id - ] += argument_diff - else: - delta = None - else: - delta = None - # re-set stuff pertaining to progress in the current tool - self.current_tool_id = len(tool_call_arr) - 1 - self.current_tool_name_sent = False - self.streamed_args_for_tool.append("") - return delta - - # if the current tool name hasn't been sent, send if available - # - otherwise send nothing - elif not self.current_tool_name_sent: - function_name = current_tool_call.get("name") - if function_name: - delta = ChatCompletionStreamResponseDelta( - tool_calls=[ - ChatCompletionMessageToolCallChunk( - index=self.current_tool_id, - type="function", - id=f"cmpl-{uuid.uuid1()}", - function=Function2(name=function_name).model_dump( - exclude_none=True - ), - ) - ] - ) - self.current_tool_name_sent = True - else: - delta = None - - # now we know we're on the same tool call and we're streaming - # arguments - else: - cur_arguments = current_tool_call.get("arguments") - delta = None - - if cur_arguments: - sent = len(self.streamed_args_for_tool[self.current_tool_id]) - cur_args_json = json.dumps(cur_arguments) - prev_arguments = self.prev_tool_call_arr[self.current_tool_id].get( - "arguments" - ) - - argument_diff = None - if is_complete[self.current_tool_id]: - argument_diff = cur_args_json[sent:] - elif prev_arguments: - prev_args_json = json.dumps(prev_arguments) - if cur_args_json != prev_args_json: - prefix = find_common_prefix(prev_args_json, cur_args_json) - argument_diff = prefix[sent:] - - if argument_diff is not None: - delta = ChatCompletionStreamResponseDelta( - tool_calls=[ - ChatCompletionMessageToolCallChunk( - index=self.current_tool_id, - function=Function2( - arguments=argument_diff - ).model_dump(exclude_none=True), - ) - ] - ) - self.streamed_args_for_tool[ - self.current_tool_id - ] += argument_diff - - self.prev_tool_call_arr = tool_call_arr - return delta - - except Exception: - return None diff --git a/python/openai/openai_frontend/engine/utils/tool_call_parsers/mistral_tool_call_parser.py b/python/openai/openai_frontend/engine/utils/tool_call_parsers/mistral_tool_call_parser.py deleted file mode 100644 index e3af3944e9..0000000000 --- a/python/openai/openai_frontend/engine/utils/tool_call_parsers/mistral_tool_call_parser.py +++ /dev/null @@ -1,318 +0,0 @@ -# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -# Adapted from -# https://github.com/vllm-project/vllm/blob/main/vllm/entrypoints/openai/tool_parsers/mistral_tool_parser.py -# Copyright 2024 The vLLM team. -import json -import re -from random import choices -from string import ascii_letters, digits -from typing import Dict, List, Union - -import partial_json_parser -from engine.utils.tokenizer import AnyTokenizer -from engine.utils.tool_call_parsers.tool_call_parser import ( - ToolCallParser, - ToolParserManager, -) -from partial_json_parser.core.options import Allow -from schemas.openai import ( - ChatCompletionMessageToolCall, - ChatCompletionMessageToolCallChunk, - ChatCompletionMessageToolCalls, - ChatCompletionResponseMessage, - ChatCompletionStreamResponseDelta, - Function1, - Function2, -) - -from .utils import extract_intermediate_diff - -ALPHANUMERIC = ascii_letters + digits - - -def generate_mistral_random_id(): - # Mistral Tool Call Ids must be alphanumeric with a maximum length of 9. - # https://github.com/mistralai/mistral-common/blob/21ee9f6cee3441e9bb1e6ed2d10173f90bd9b94b/src/mistral_common/protocol/instruct/validator.py#L299 - return "".join(choices(ALPHANUMERIC, k=9)) - - -@ToolParserManager.register_module("mistral") -class MistralToolParser(ToolCallParser): - def __init__(self, tokenizer: AnyTokenizer): - super().__init__(tokenizer) - - # initialize properties used for state when parsing tool calls in - # streaming mode - self.prev_tool_call_arr: List[Dict] = [] - self.current_tool_id: int = -1 - self.current_tool_name_sent: bool = False - self.streamed_args_for_tool: List[ - str - ] = [] # map what has been streamed for each tool so far to a list - self.bot_token = "[TOOL_CALLS]" - self.tool_call_regex = re.compile(r"\[{.*}\]", re.DOTALL) - - def parse_tool_calls( - self, full_text: str, role: str, backend: str - ) -> ChatCompletionResponseMessage: - """ - Extract the tool calls from a complete model response. Requires - find-and-replacing single quotes with double quotes for JSON parsing, - make sure your tool call arguments don't ever include quotes! - """ - - # case -- if a tool call token is not present, return a text response - if not (full_text.startswith(self.bot_token) or full_text.startswith("[")): - return ChatCompletionResponseMessage( - tool_calls=None, content=full_text, role=role - ) - - # first remove the BOT token - tool_content = full_text.replace(self.bot_token, "").strip() - try: - # we first try to directly load the json as parsing very nested - # jsons is difficult - try: - function_call_arr = json.loads(tool_content) - except json.JSONDecodeError: - # use a regex to find the part corresponding to the tool call. - # NOTE: This use case should not happen if the model is trained - # correctly. It's a easy possible fix so it's included, but - # can be brittle for very complex / highly nested tool calls - raw_tool_call = self.tool_call_regex.findall(tool_content)[0] - function_call_arr = json.loads(raw_tool_call) - - # Tool Call - tool_calls = ChatCompletionMessageToolCalls( - root=[ - ChatCompletionMessageToolCall( - id=generate_mistral_random_id(), - type="function", - function=Function1( - name=raw_function_call["name"], - # function call args are JSON but as a string - arguments=json.dumps( - raw_function_call["arguments"], ensure_ascii=False - ), - ), - ) - for raw_function_call in function_call_arr - ] - ) - - # get any content before the tool call - content = ( - full_text.split(self.bot_token)[0] - if full_text.startswith(self.bot_token) - else "" - ) - return ChatCompletionResponseMessage( - tool_calls=tool_calls, content=content, role=role - ) - - except Exception: - # return information to just treat the tool call as regular JSON - return ChatCompletionResponseMessage( - tool_calls=None, content=full_text, role=role - ) - - def parse_tool_calls_streaming( - self, current_text: str, delta_text: str, backend: str - ) -> Union[ChatCompletionStreamResponseDelta, None]: - # if the tool call token is not in the tokens generated so far, append - # output to contents since it's not a tool - # tensorrt_llm backend likely doesn't generate the bos token - if not (self.bot_token in current_text or "[" in current_text): - return ChatCompletionStreamResponseDelta(content=delta_text) - - # handle if we detected the BOT token which means the start of tool - # calling - if self.bot_token == delta_text.strip(): - # if it's the only token, return None, so we don't send a chat - # completion any don't send a control token - return None - - flags = Allow.ALL if self.current_tool_name_sent else Allow.ALL & ~Allow.STR - - try: - # replace BOT token with empty string, and convert single quotes - # to double to allow parsing as JSON since mistral uses single - # quotes instead of double for tool calls - parsable_arr = current_text.split(self.bot_token)[-1] - - # tool calls are generated in an array, so do partial JSON - # parsing on the entire array - try: - tool_call_arr: List[Dict] = partial_json_parser.loads( - parsable_arr, flags - ) - except partial_json_parser.core.exceptions.MalformedJSON: - return None - - # select as the current tool call the one we're on the state at - - current_tool_call: Dict = ( - tool_call_arr[self.current_tool_id] if len(tool_call_arr) > 0 else {} - ) - - # case -- if no tokens have been streamed for the tool, e.g. - # only the array brackets, stream nothing - if len(tool_call_arr) == 0: - return None - - # case: we are starting a new tool in the array - # -> array has > 0 length AND length has moved past cursor - elif ( - len(tool_call_arr) > 0 and len(tool_call_arr) > self.current_tool_id + 1 - ): - # if we're moving on to a new call, first make sure we - # haven't missed anything in the previous one that was - # auto-generated due to JSON completions, but wasn't - # streamed to the client yet. - if self.current_tool_id >= 0: - diff: Union[str, None] = current_tool_call.get("arguments") - - if diff: - diff = json.dumps(diff, ensure_ascii=False).replace( - self.streamed_args_for_tool[self.current_tool_id], "" - ) - - delta = ChatCompletionStreamResponseDelta( - tool_calls=[ - ChatCompletionMessageToolCallChunk( - index=self.current_tool_id, - function=Function2(arguments=diff).model_dump( - exclude_none=True - ), - ) - ] - ) - self.streamed_args_for_tool[self.current_tool_id] += diff - else: - delta = None - else: - delta = None - # re-set stuff pertaining to progress in the current tool - self.current_tool_id = len(tool_call_arr) - 1 - self.current_tool_name_sent = False - self.streamed_args_for_tool.append("") - return delta - - # case: update an existing tool - this is handled below - - # if the current tool name hasn't been sent, send if available - # - otherwise send nothing - if not self.current_tool_name_sent: - function_name = current_tool_call.get("name") - if function_name: - delta = ChatCompletionStreamResponseDelta( - tool_calls=[ - ChatCompletionMessageToolCallChunk( - index=self.current_tool_id, - type="function", - id=generate_mistral_random_id(), - function=Function2(name=function_name).model_dump( - exclude_none=True - ), - ) - ] - ) - self.current_tool_name_sent = True - else: - delta = None - - # now we know we're on the same tool call and we're streaming - # arguments - else: - prev_arguments = self.prev_tool_call_arr[self.current_tool_id].get( - "arguments" - ) - cur_arguments = current_tool_call.get("arguments") - - new_text = delta_text.replace("'", '"') - if '"}' in new_text: - new_text = new_text[: new_text.rindex('"}')] - - if not cur_arguments and not prev_arguments: - delta = None - elif not cur_arguments and prev_arguments: - delta = None - elif cur_arguments and not prev_arguments: - cur_arguments_json = json.dumps(cur_arguments, ensure_ascii=False)[ - :-2 - ] - - if new_text not in cur_arguments_json: - return None - arguments_delta = cur_arguments_json[ - : cur_arguments_json.rindex(new_text) + len(new_text) - ] - delta = ChatCompletionStreamResponseDelta( - tool_calls=[ - ChatCompletionMessageToolCallChunk( - index=self.current_tool_id, - function=Function2( - arguments=arguments_delta - ).model_dump(exclude_none=True), - ) - ] - ) - self.streamed_args_for_tool[self.current_tool_id] += arguments_delta - - elif cur_arguments and prev_arguments: - cur_args_json = json.dumps(cur_arguments, ensure_ascii=False) - prev_args_json = json.dumps(prev_arguments, ensure_ascii=False) - - argument_diff = extract_intermediate_diff( - cur_args_json, prev_args_json - ) - delta = ChatCompletionStreamResponseDelta( - tool_calls=[ - ChatCompletionMessageToolCallChunk( - index=self.current_tool_id, - function=Function2(arguments=argument_diff).model_dump( - exclude_none=True - ), - ) - ] - ) - self.streamed_args_for_tool[self.current_tool_id] += argument_diff - else: - # try parsing it with regular JSON - if it works we're - # at the end, and we need to send the difference between - # tokens streamed so far and the valid JSON - delta = None - - # check to see if the name is defined and has been sent. if so, - # stream the name - otherwise keep waiting - # finish by setting old and returning None as base case - self.prev_tool_call_arr = tool_call_arr - return delta - - except Exception: - return None diff --git a/python/openai/openai_frontend/engine/utils/tool_call_parsers/tool_call_parser.py b/python/openai/openai_frontend/engine/utils/tool_call_parsers/tool_call_parser.py deleted file mode 100644 index 5ae192eb6c..0000000000 --- a/python/openai/openai_frontend/engine/utils/tool_call_parsers/tool_call_parser.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -# Adapted from -# https://github.com/vllm-project/vllm/blob/main/vllm/entrypoints/openai/tool_parsers/abstract_tool_parser.py -# Copyright 2024 The vLLM team. -from typing import Callable, Dict, List, Optional, Union - -from engine.utils.tokenizer import AnyTokenizer -from schemas.openai import ( - ChatCompletionMessageToolCalls, - ChatCompletionStreamResponseDelta, -) - - -class ToolCallParser: - """The Base Tool Call Parser for parsing the Tool Call from the responses, - Two inferfaces are supported: the one-time parser for synchronized response - and streaming parser for streaming response. - """ - - def __init__(self, tokenizer: AnyTokenizer): - self.prev_tool_call_arr: List[Dict] = [] - # the index of the tool call that is currently being parsed - self.current_tool_id: int = -1 - self.current_tool_name_sent: bool = False - self.streamed_args_for_tool: List[str] = [] - - self.model_tokenizer = tokenizer - - def parse_tool_calls( - self, full_text: str, role: str, backend: str - ) -> ChatCompletionMessageToolCalls: - raise NotImplementedError( - "BaseToolCallParser.parse_tool_calls has not been implemented!" - ) - - def parse_tool_calls_streaming( - self, current_text: str, delta_text: str, backend: str - ) -> ChatCompletionStreamResponseDelta: - raise NotImplementedError( - "BaseToolCallParser.parse_tool_calls_streaming has not been implemented!" - ) - - -class ToolParserManager: - tool_parsers: dict[str, type] = {} - - @classmethod - def get_tool_parser_cls(cls, name) -> type: - if name in cls.tool_parsers: - return cls.tool_parsers[name] - - raise KeyError(f"tool parser: '{name}' not found in tool_call_parsers") - - @classmethod - def _register_module( - cls, - module: type, - module_name: Optional[Union[str, list[str]]] = None, - force: bool = True, - ) -> None: - if not issubclass(module, ToolCallParser): - raise TypeError( - f"module must be subclass of ToolCallParser, but got {type(module)}" - ) - if module_name is None: - module_name = module.__name__ - if isinstance(module_name, str): - module_name = [module_name] - for name in module_name: - if not force and name in cls.tool_parsers: - existed_module = cls.tool_parsers[name] - raise KeyError( - f"{name} is already registered " f"at {existed_module.__module__}" - ) - cls.tool_parsers[name] = module - - @classmethod - def register_module( - cls, - name: Optional[Union[str, list[str]]] = None, - force: bool = True, - module: Union[type, None] = None, - ) -> Union[type, Callable]: - """ - Register module with the given name or name list. it can be used as a - decoder(with module as None) or normal function(with module as not - None). - """ - if not isinstance(force, bool): - raise TypeError(f"force must be a boolean, but got {type(force)}") - - # raise the error ahead of time - if not (name is None or isinstance(name, str)): - raise TypeError( - "name must be None, an instance of str, " f"but got {type(name)}" - ) - - # use it as a normal method: x.register_module(module=SomeClass) - if module is not None: - cls._register_module(module=module, module_name=name, force=force) - return module - - # use it as a decorator: @x.register_module() - def _register(module): - cls._register_module(module=module, module_name=name, force=force) - return module - - return _register diff --git a/python/openai/openai_frontend/engine/utils/tool_call_parsers/utils.py b/python/openai/openai_frontend/engine/utils/tool_call_parsers/utils.py deleted file mode 100644 index 60513d3f8f..0000000000 --- a/python/openai/openai_frontend/engine/utils/tool_call_parsers/utils.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -# Adapted from -# https://github.com/vllm-project/vllm/blob/main/vllm/entrypoints/openai/tool_parsers/utils.py -# Copyright 2024 The vLLM team. - -import json -from typing import Any - -import partial_json_parser -from partial_json_parser.core.options import Allow - - -# partial_json_parser doesn't support extra data and -# JSONDecorder.raw_decode doesn't support partial JSON -def partial_json_loads(input_str: str, flags: Allow) -> tuple[Any, int]: - try: - return (partial_json_parser.loads(input_str, flags), len(input_str)) - except json.JSONDecodeError as e: - if "Extra data" in e.msg: - dec = json.JSONDecoder() - return dec.raw_decode(input_str) - raise - - -def is_complete_json(input_str: str) -> bool: - try: - json.loads(input_str) - return True - except json.JSONDecodeError: - return False - - -def find_common_prefix(s1: str, s2: str) -> str: - """ - Finds a common prefix that is shared between two strings, if there is one. - Order of arguments is NOT important. - - This function is provided as a UTILITY for extracting information from JSON - generated by partial_json_parser, to help in ensuring that the right tokens - are returned in streaming, so that close-quotes, close-brackets and - close-braces are not returned prematurely. - - e.g. find_common_prefix('{"fruit": "ap"}', '{"fruit": "apple"}') -> - '{"fruit": "ap' - """ - prefix = "" - min_length = min(len(s1), len(s2)) - for i in range(0, min_length): - if s1[i] == s2[i]: - prefix += s1[i] - else: - break - return prefix - - -def find_common_suffix(s1: str, s2: str) -> str: - """ - Finds a common suffix shared between two strings, if there is one. Order of - arguments is NOT important. - Stops when the suffix ends OR it hits an alphanumeric character - - e.g. find_common_suffix('{"fruit": "ap"}', '{"fruit": "apple"}') -> '"}' - """ - suffix = "" - min_length = min(len(s1), len(s2)) - for i in range(1, min_length + 1): - if s1[-i] == s2[-i] and not s1[-i].isalnum(): - suffix = s1[-i] + suffix - else: - break - return suffix - - -def extract_intermediate_diff(curr: str, old: str) -> str: - """ - Given two strings, extract the difference in the middle between two strings - that are known to have a common prefix and/or suffix. - - This function is provided as a UTILITY for extracting information from JSON - generated by partial_json_parser, to help in ensuring that the right tokens - are returned in streaming, so that close-quotes, close-brackets and - close-braces are not returned prematurely. The order of arguments IS - important - the new version of the partially-parsed JSON must be the first - argument, and the second argument must be from the previous generation. - - What it returns, is tokens that should be streamed to the client. - - e.g. extract_intermediate_diff('{"fruit": "apple"}', '{"fruit": "ap"}') - -> 'ple' - - """ - suffix = find_common_suffix(curr, old) - - old = old[::-1].replace(suffix[::-1], "", 1)[::-1] - prefix = find_common_prefix(curr, old) - diff = curr - if len(suffix): - diff = diff[::-1].replace(suffix[::-1], "", 1)[::-1] - - if len(prefix): - # replace the prefix only once in case it's mirrored - diff = diff.replace(prefix, "", 1) - - return diff diff --git a/python/openai/openai_frontend/engine/utils/triton.py b/python/openai/openai_frontend/engine/utils/triton.py index 91925e16b0..2ec8cce7d5 100644 --- a/python/openai/openai_frontend/engine/utils/triton.py +++ b/python/openai/openai_frontend/engine/utils/triton.py @@ -1,4 +1,4 @@ -# Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -23,57 +23,17 @@ # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + import ctypes -import json -import os -import re -import sys -import traceback -from dataclasses import asdict, dataclass, field -from enum import Enum -from pathlib import Path -from typing import Dict, Iterable, List, Optional, Union +from typing import Iterable, List import numpy as np import tritonserver -from pydantic import BaseModel -from schemas.openai import ( - ChatCompletionNamedToolChoice, - ChatCompletionTokenLogprob, - ChatCompletionToolChoiceOption1, - CompletionUsage, - CreateChatCompletionRequest, - CreateCompletionRequest, - CreateEmbeddingRequest, - EmbeddingUsage, - Logprobs, - TopLogprob, -) -from utils.utils import ClientError, ServerError - - -class RequestKind(Enum): - GENERATION = 1 - EMBEDDING = 2 - +from schemas.openai import CreateChatCompletionRequest, CreateCompletionRequest -@dataclass -class TritonLoraConfig: - name: str - # Unique fields for TensorRT-LLM backend - task_id: Optional[int] = None - path: Optional[str] = None - is_registered: Optional[bool] = False - - -def _create_vllm_generate_request( - model, - prompt, - request: CreateChatCompletionRequest | CreateCompletionRequest, - lora_config: TritonLoraConfig | None, - echo_tensor_name: str | None, - default_max_tokens: int, +def _create_vllm_inference_request( + model, prompt, request: CreateChatCompletionRequest | CreateCompletionRequest ): inputs = {} # Exclude non-sampling parameters so they aren't passed to vLLM @@ -95,71 +55,15 @@ def _create_vllm_generate_request( "function_call", "functions", "suffix", - "max_completion_tokens", - # will be handled explicitly - "max_tokens", - "logprobs", - "top_logprobs", - # not supported for vLLM backend (removed from vLLM V1) but supported for TRT-LLM/Python backend - "best_of", } # NOTE: The exclude_none is important, as internals may not support # values of NoneType at this time. - sampling_parameters = request.model_dump( + sampling_parameters = request.model_dump_json( exclude=excludes, exclude_none=True, ) - request_logprobs = False - # Indicates CreateChatCompletionRequest - if hasattr(request, "max_completion_tokens"): - if request.max_completion_tokens is not None: - sampling_parameters["max_tokens"] = request.max_completion_tokens - # Fallback to deprecated request.max_tokens - elif request.max_tokens is not None: - sampling_parameters["max_tokens"] = request.max_tokens - # If neither is set, use a default value for max_tokens - else: - sampling_parameters["max_tokens"] = default_max_tokens - - # Handle logprobs for chat completions - # OpenAI API: logprobs (bool), top_logprobs (int 0-20) - # vLLM API: logprobs (int) - number of top token logprobs to return - if request.logprobs and request.top_logprobs is not None: - sampling_parameters["logprobs"] = request.top_logprobs - request_logprobs = True - elif request.logprobs: - # If logprobs=True but top_logprobs not specified, default to 1 - sampling_parameters["logprobs"] = 1 - request_logprobs = True - # Indicates CreateCompletionRequest - else: - if request.max_tokens is not None: - sampling_parameters["max_tokens"] = request.max_tokens - else: - sampling_parameters["max_tokens"] = default_max_tokens - - # Handle logprobs for completions - # OpenAI API: logprobs (int 0-5) - number of top token log probs - # vLLM API: logprobs (int) - same behavior, pass directly - if request.logprobs is not None and request.logprobs > 0: - sampling_parameters["logprobs"] = request.logprobs - request_logprobs = True - inputs["return_logprobs"] = np.bool_([request_logprobs]) - - if lora_config is not None: - sampling_parameters["lora_name"] = lora_config.name - - guided_json = _get_guided_json_from_tool(request) - if guided_json is not None: - from vllm.sampling_params import StructuredOutputsParams - - sampling_parameters["structured_outputs"] = json.dumps( - asdict(StructuredOutputsParams(json=guided_json)) - ) - sampling_parameters = json.dumps(sampling_parameters) - exclude_input_in_output = True echo = getattr(request, "echo", None) if echo is not None: @@ -167,43 +71,21 @@ def _create_vllm_generate_request( inputs["text_input"] = [prompt] inputs["stream"] = np.bool_([request.stream]) - inputs[echo_tensor_name] = np.bool_([exclude_input_in_output]) + inputs["exclude_input_in_output"] = np.bool_([exclude_input_in_output]) # Pass sampling_parameters as serialized JSON string input to support List # fields like 'stop' that aren't supported by TRITONSERVER_Parameters yet. inputs["sampling_parameters"] = [sampling_parameters] - inputs["return_num_input_tokens"] = np.bool_([True]) - inputs["return_num_output_tokens"] = np.bool_([True]) return model.create_request(inputs=inputs) -def _create_trtllm_generate_request( - model, - prompt, - request: CreateChatCompletionRequest | CreateCompletionRequest, - lora_config: TritonLoraConfig | None, - echo_tensor_name: str | None, - default_max_tokens: int, +def _create_trtllm_inference_request( + model, prompt, request: CreateChatCompletionRequest | CreateCompletionRequest ): inputs = {} inputs["text_input"] = [[prompt]] inputs["stream"] = np.bool_([[request.stream]]) - - # Indicates CreateChatCompletionRequest - if hasattr(request, "max_completion_tokens"): - if request.max_completion_tokens is not None: - inputs["max_tokens"] = np.int32([[request.max_completion_tokens]]) - # Fallback to deprecated request.max_tokens - elif request.max_tokens is not None: - inputs["max_tokens"] = np.int32([[request.max_tokens]]) - # If neither is set, use a default value for max_tokens - else: - inputs["max_tokens"] = np.int32([[default_max_tokens]]) - # Indicates CreateCompletionRequest - elif request.max_tokens is not None: + if request.max_tokens: inputs["max_tokens"] = np.int32([[request.max_tokens]]) - else: - inputs["max_tokens"] = np.int32([[default_max_tokens]]) - if request.stop: if isinstance(request.stop, str): request.stop = [request.stop] @@ -216,68 +98,14 @@ def _create_trtllm_generate_request( if request.presence_penalty is not None: inputs["presence_penalty"] = np.float32([[request.presence_penalty]]) if request.seed is not None: - inputs["seed"] = np.uint64([[request.seed]]) + inputs["random_seed"] = np.uint64([[request.seed]]) if request.temperature is not None: inputs["temperature"] = np.float32([[request.temperature]]) - # Only limited TRT-LLM models support "echo" (inflight_batcher_llm, disaggregated_serving, llmapi) - echo = getattr(request, "echo", None) - if echo is not None and echo_tensor_name is not None: - inputs[echo_tensor_name] = np.bool_([[not echo]]) - - guided_json = _get_guided_json_from_tool(request) - if guided_json is not None: - inputs["guided_decoding_guide_type"] = [["json_schema"]] - inputs["guided_decoding_guide"] = [[guided_json]] - - if lora_config is not None: - # To perform inference with a specific LoRA for the first time `lora_task_id` `lora_weights` and `lora_config` must all be given. - # The LoRA will be cached, so that subsequent requests for the same task only require `lora_task_id`. - inputs["lora_task_id"] = np.uint64([[lora_config.task_id]]) - if not lora_config.is_registered: - lora_weights_data = np.load( - os.path.join(lora_config.path, "model.lora_weights.npy") - ) - lora_config_data = np.load( - os.path.join(lora_config.path, "model.lora_config.npy") - ) - inputs["lora_weights"] = lora_weights_data - inputs["lora_config"] = lora_config_data - lora_config.is_registered = True - - inputs["return_num_input_tokens"] = np.bool_([[True]]) - inputs["return_num_output_tokens"] = np.bool_([[True]]) + # FIXME: TRT-LLM doesn't currently support runtime changes of 'echo' and it + # is configured at model load time, so we don't handle it here for now. return model.create_request(inputs=inputs) -def _create_vllm_embedding_request( - model, - request: CreateEmbeddingRequest, -): - inputs = {} - embedding_request = {} - embedding_request["input"] = request.input - - pooling_params = {} - dims = request.dimensions - if dims is not None: - pooling_params["dimensions"] = [dims] - embedding_request["pooling_params"] = pooling_params - - inputs["embedding_request"] = [json.dumps(embedding_request)] - inputs["return_num_input_tokens"] = np.bool_([True]) - inputs["return_num_output_tokens"] = np.bool_([True]) - return model.create_request(inputs=inputs) - - -def _create_trtllm_embedding_request( - model, - request: CreateEmbeddingRequest, -): - raise ClientError( - "TRT-LLM backend and Python backend do not support embedding requests" - ) - - def _construct_string_from_pointer(pointer: int, size: int) -> str: """Constructs a Python string from a C pointer and size.""" @@ -308,11 +136,11 @@ def _to_string(tensor: tritonserver.Tensor) -> str: # there is only a single string, so enforce it to avoid obscure errors. volume = _get_volume(tensor.shape) if volume != 1: - raise ServerError( + raise Exception( f"Expected to find 1 string in the output, found {volume} instead." ) if tensor.size < 4: - raise ServerError( + raise Exception( f"Expected string buffer to contain its serialized byte size, but found size of {tensor.size}." ) @@ -320,97 +148,6 @@ def _to_string(tensor: tritonserver.Tensor) -> str: return _construct_string_from_pointer(tensor.data_ptr + 4, tensor.size - 4) -@dataclass -class _StreamingUsageAccumulator: - """Helper class to accumulate token usage from a streaming response.""" - - backend: str - prompt_tokens: int = 0 - completion_tokens: int = 0 - _prompt_tokens_set: bool = field(init=False, default=False) - - def update(self, response: tritonserver.InferenceResponse): - """Extracts usage from a response and updates the token counts.""" - usage = _get_usage_from_response(response, self.backend, RequestKind.GENERATION) - if usage: - # The prompt_tokens is received with every chunk but should only be set once. - if not self._prompt_tokens_set: - self.prompt_tokens = usage.prompt_tokens - self._prompt_tokens_set = True - self.completion_tokens += usage.completion_tokens - - def get_final_usage(self) -> Optional[CompletionUsage]: - """ - Returns the final populated CompletionUsage object if any tokens were tracked. - """ - # If _prompt_tokens_set is True, it means we have received and processed - # at least one valid usage payload. - if self._prompt_tokens_set: - return CompletionUsage( - prompt_tokens=self.prompt_tokens, - completion_tokens=self.completion_tokens, - total_tokens=self.prompt_tokens + self.completion_tokens, - ) - return None - - -def _get_usage_from_response( - response: tritonserver._api._response.InferenceResponse, - backend: str, - request_type: RequestKind, -) -> Optional[CompletionUsage | EmbeddingUsage]: - """ - Extracts token usage statistics from a Triton inference response. - """ - prompt_tokens = None - completion_tokens = None - - if ( - "num_input_tokens" in response.outputs - and "num_output_tokens" in response.outputs - ): - input_token_tensor = response.outputs["num_input_tokens"] - output_token_tensor = response.outputs["num_output_tokens"] - - if input_token_tensor.data_type == tritonserver.DataType.UINT32: - prompt_tokens_ptr = ctypes.cast( - input_token_tensor.data_ptr, ctypes.POINTER(ctypes.c_uint32) - ) - prompt_tokens = prompt_tokens_ptr[0] - elif input_token_tensor.data_type == tritonserver.DataType.INT32: - prompt_tokens_ptr = ctypes.cast( - input_token_tensor.data_ptr, ctypes.POINTER(ctypes.c_int32) - ) - prompt_tokens = prompt_tokens_ptr[0] - - if output_token_tensor.data_type == tritonserver.DataType.UINT32: - completion_tokens_ptr = ctypes.cast( - output_token_tensor.data_ptr, ctypes.POINTER(ctypes.c_uint32) - ) - completion_tokens = completion_tokens_ptr[0] - elif output_token_tensor.data_type == tritonserver.DataType.INT32: - completion_tokens_ptr = ctypes.cast( - output_token_tensor.data_ptr, ctypes.POINTER(ctypes.c_int32) - ) - completion_tokens = completion_tokens_ptr[0] - - if prompt_tokens is not None: - if request_type == RequestKind.GENERATION and completion_tokens is not None: - total_tokens = prompt_tokens + completion_tokens - return CompletionUsage( - prompt_tokens=prompt_tokens, - completion_tokens=completion_tokens, - total_tokens=total_tokens, - ) - elif request_type == RequestKind.EMBEDDING: - return EmbeddingUsage( - prompt_tokens=prompt_tokens, - total_tokens=prompt_tokens, - ) - - return None - - # TODO: Use tritonserver.InferenceResponse when support is published def _get_output(response: tritonserver._api._response.InferenceResponse) -> str: if "text_output" in response.outputs: @@ -426,305 +163,13 @@ def _get_output(response: tritonserver._api._response.InferenceResponse) -> str: return "" -def _get_logprobs_from_response( - response: tritonserver._api._response.InferenceResponse, -) -> Optional[List[Dict]]: - """ - Extracts logprobs from a Triton inference response (vLLM backend). - - Returns: - List of dictionaries containing logprobs data, or None if not available. - Format: [ - { - token_id: { - "logprob": float, - "rank": int, - "decoded_token": str - } - }, - ... - ] - """ - if "logprobs" not in response.outputs: - return None - - logprobs_tensor = response.outputs["logprobs"] - if logprobs_tensor is None: - return None - - # The logprobs are stored as JSON string (vLLM backend) - logprobs_str = _to_string(logprobs_tensor) - - if logprobs_str == "null": - return None - - try: - logprobs_data = json.loads(logprobs_str) - return logprobs_data - except json.JSONDecodeError: - return None - - -def _get_openai_chat_format_logprobs_from_vllm_response( - response: tritonserver._api._response.InferenceResponse, -) -> Optional[List[ChatCompletionTokenLogprob]]: - """ - Convert logprobs from a Triton inference response (vLLM backend) to OpenAI chat completion format. - - Args: - response: Triton inference response containing logprobs output. - - Returns: - List of ChatCompletionTokenLogprob objects, or None if no logprobs available. - """ - vllm_logprobs = _get_logprobs_from_response(response) - - if not vllm_logprobs: - return None - - openai_logprobs = [] - for token_logprobs_dict in vllm_logprobs: - if not token_logprobs_dict: - continue - - # Sort by rank to identify the selected token (rank=1 is always the chosen token) - sorted_tokens = sorted( - token_logprobs_dict.items(), key=lambda x: x[1].get("rank", sys.maxsize) - ) - - # The first token (lowest rank) is the selected token - selected_token_id, selected_token_data = sorted_tokens[0] - selected_token = selected_token_data["decoded_token"] - selected_logprob = selected_token_data["logprob"] - - # Convert to bytes representation - token_bytes = list(selected_token.encode("utf-8")) - - top_logprobs_list = [] - for token_id, token_data in sorted_tokens: - decoded_token = token_data["decoded_token"] - top_logprobs_list.append( - TopLogprob( - token=decoded_token, - logprob=token_data["logprob"], - bytes=list(decoded_token.encode("utf-8")), - ) - ) - - openai_logprobs.append( - ChatCompletionTokenLogprob( - token=selected_token, - logprob=selected_logprob, - bytes=token_bytes, - top_logprobs=top_logprobs_list, - ) - ) - - return openai_logprobs - - -def _get_openai_completion_format_logprobs_from_vllm_response( - response: tritonserver._api._response.InferenceResponse, -) -> Optional[Logprobs]: - """ - Convert logprobs from a Triton inference response (vLLM backend) to OpenAI completion format. - - Args: - response: Triton inference response containing logprobs output. - - Returns: - Logprobs object for completions API, or None if no logprobs available. - """ - vllm_logprobs = _get_logprobs_from_response(response) - - if not vllm_logprobs: - return None - - text_offset = [] - token_logprobs = [] - tokens = [] - top_logprobs = [] - - current_offset = 0 - for token_logprobs_dict in vllm_logprobs: - if not token_logprobs_dict: - continue - - # Sort by rank to identify the selected token (rank=1 is always the chosen token) - sorted_tokens = sorted( - token_logprobs_dict.items(), key=lambda x: x[1].get("rank", sys.maxsize) - ) - - # The first token (lowest rank) is the selected token - selected_token_id, selected_token_data = sorted_tokens[0] - selected_token = selected_token_data["decoded_token"] - selected_logprob = selected_token_data["logprob"] - - text_offset.append(current_offset) - token_logprobs.append(selected_logprob) - tokens.append(selected_token) - - # Build top_logprobs dict for this position - top_logprobs_dict = {} - for token_id, token_data in sorted_tokens: - decoded_token = token_data["decoded_token"] - top_logprobs_dict[decoded_token] = token_data["logprob"] - top_logprobs.append(top_logprobs_dict) - - current_offset += len(selected_token) - - return Logprobs( - text_offset=text_offset, - token_logprobs=token_logprobs, - tokens=tokens, - top_logprobs=top_logprobs, - ) - - def _validate_triton_responses_non_streaming( responses: List[tritonserver._api._response.InferenceResponse], ): num_responses = len(responses) - if 1 <= num_responses <= 2: - if responses[-1].final != True: - raise ServerError("Unexpected internal error with incorrect response flags") - else: - raise ServerError( - f"Unexpected number of responses: {num_responses}, expected 1 or 2." - ) - - -def _get_guided_json_from_tool( - request: CreateChatCompletionRequest | CreateCompletionRequest, -) -> Optional[Union[str, dict, BaseModel]]: - if isinstance(request, CreateChatCompletionRequest): - if request.tool_choice is None or not request.tools: - return None - - if type(request.tool_choice.root) is ChatCompletionNamedToolChoice: - tool_name = request.tool_choice.root.function.name - elif request.tool_choice.root == ChatCompletionToolChoiceOption1.required: - tool_name = request.tools[0].function.name - else: - return None - - tools = {tool.function.name: tool.function for tool in request.tools} - if tool_name not in tools: - raise ClientError(f"Tool '{tool_name}' has not been passed in `tools`.") - tool = tools[tool_name] - return tool.parameters.model_dump_json() - - return None - - -def _validate_lora_path_trtllm(repo_path: str, lora_path: str, lora_name: str): - if os.path.isabs(lora_path): - raise ValueError( - f"LoRA path '{lora_path}' for '{lora_name}' must be a relative path inside its model repository" - ) - - # NOTE: Error messages should never contain the real/absolute paths. - realpath_repo = os.path.realpath(repo_path) - realpath_lora = os.path.realpath(os.path.join(realpath_repo, lora_path)) - # Always check if the LoRA path is inside the model repository before checking its existence. - if os.path.commonpath([realpath_repo, realpath_lora]) != realpath_repo: - raise ValueError( - f"LoRA path '{lora_path}' for '{lora_name}' must be inside its model repository" - ) - if not os.path.exists(realpath_lora): - raise ServerError( - f"LoRA directory '{lora_path}' not found for '{lora_name}' in its model repository" - ) - - # Check if the files exist - for lora_file in ["model.lora_weights.npy", "model.lora_config.npy"]: - lora_file_path = os.path.join(realpath_lora, lora_file) - if not os.path.exists(lora_file_path): - raise ServerError( - f"LoRA file '{lora_file}' not found for '{lora_name}' at path: {lora_file_path}" - ) - - -def _parse_lora_configs( - model_repository: str | list[str], model_name: str, model_version: int, backend: str -) -> None | List[tuple[str, str]]: - if ( - len(model_name) == 0 - or model_name.isspace() - or "/" in model_name - or "\\" in model_name - ): - raise ValueError( - f"Invalid model name: '{model_name}'. Model names must be valid file-system-path segment names." - ) - - lora_configs = [] - lora_task_id = 1 - repo_paths = model_repository - if isinstance(repo_paths, str): - repo_paths = [repo_paths] - for repo_path in repo_paths: - model_path = os.path.join(repo_path, model_name) - if (not Path(model_path).is_relative_to(repo_path)) or ( - os.path.normpath(model_path) != model_path - ): - raise ValueError( - f"Invalid model name: '{model_name}'. Model names must be valid file-system-path segment names." - ) - - model_path = os.path.normpath(model_path) - if not os.path.isdir(model_path): - # Cloud path? - return None - if model_version <= 0: - for version_path in os.listdir(model_path): - version = os.path.basename(version_path) - if re.fullmatch(r"^[0-9]+$", version) is None: - continue - model_version = max(model_version, int(version)) - if model_version <= 0: - # Model directory is malformed? - return None - version_path = os.path.join(model_path, str(model_version)) - lora_config_path = os.path.join(version_path, "multi_lora.json") - - if backend == "vllm": - is_lora_enabled = False - model_file_path = os.path.join(version_path, "model.json") - try: - with open(model_file_path, "r") as f: - config = json.load(f) - if "enable_lora" in config: - # The value could be a string or a bool. - is_lora_enabled = str(config["enable_lora"]).lower() == "true" - except Exception: - # Model directory or model.json is malformed? - return None - if is_lora_enabled != True: - continue - else: - # TRT-LLM backend does not use model.json - if not os.path.exists(lora_config_path): - continue - - try: - with open(lora_config_path, "r") as f: - lora_config = json.load(f) - for lora_name, lora_path in lora_config.items(): - if backend == "vllm": - lora_configs.append(TritonLoraConfig(name=lora_name)) - else: - _validate_lora_path_trtllm(repo_path, lora_path, lora_name) - lora_configs.append( - TritonLoraConfig( - name=lora_name, path=lora_path, task_id=lora_task_id - ) - ) - lora_task_id += 1 - except ServerError as e: - raise e - except Exception as e: - # LoRA is enabled but its list is not provided or malformed? - print(traceback.format_exc()) - return None - return lora_configs + if num_responses == 1 and responses[0].final != True: + raise Exception("Unexpected internal error with incorrect response flags") + if num_responses == 2 and responses[-1].final != True: + raise Exception("Unexpected internal error with incorrect response flags") + if num_responses > 2: + raise Exception(f"Unexpected number of responses: {num_responses}, expected 1.") diff --git a/python/openai/openai_frontend/frontend/fastapi/middleware/__init__.py b/python/openai/openai_frontend/frontend/fastapi/middleware/__init__.py deleted file mode 100644 index f3dec540e2..0000000000 --- a/python/openai/openai_frontend/frontend/fastapi/middleware/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/python/openai/openai_frontend/frontend/fastapi/middleware/api_restriction.py b/python/openai/openai_frontend/frontend/fastapi/middleware/api_restriction.py deleted file mode 100644 index 085434ad46..0000000000 --- a/python/openai/openai_frontend/frontend/fastapi/middleware/api_restriction.py +++ /dev/null @@ -1,229 +0,0 @@ -# Copyright 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -from fastapi import Request -from fastapi.responses import JSONResponse -from starlette.middleware.base import BaseHTTPMiddleware -from utils.utils import StatusCode - -# Mapping of API to their corresponding HTTP endpoints -ENDPOINT_MAPPING = { - "inference": [ - "POST /v1/chat/completions", - "POST /v1/completions", - "POST /v1/embeddings", - ], - "model-repository": [ - "GET /v1/models", - "POST /v1/models/", - ], - "metrics": ["GET /metrics"], - "health": ["GET /health/ready"], -} - - -class RestrictedFeatures: - """ - Manages API endpoint restrictions and their authentication requirements. - - This class parses command-line arguments for restricted API configurations - and provides methods to check if specific APIs are restricted - and what authentication is required. - """ - - def __init__(self, args: list[str]): - """ - Initialize the RestrictedFeatures with command-line arguments. - - Args: - args: List of --openai-restricted-api argument strings - (e.g., [["inference", "infer-key", "infer-value"], - ["model-repository", "model-key", "model-value"]]) - """ - self._restrictions = {} - self.ParseRestrictedFeatureOption(args) - - def ParseRestrictedFeatureOption(self, args): - """ - Parse command-line arguments to extract API restrictions. - - Args: - args: List of restriction configuration strings - - Raises: - ValueError: If unknown API is specified or duplicate API configs are found - """ - for apis, key, value in args: - api_list = apis.split(",") - for api in api_list: - # Validate that the API is valid - if api not in ENDPOINT_MAPPING: - raise ValueError( - f"Unknown API '{api}'. Available APIs: {list(ENDPOINT_MAPPING.keys())}" - ) - - # Check for duplicate APIs across different arguments - if self.IsRestricted(api): - raise ValueError( - f"restricted api '{api}' can not be specified in multiple config groups" - ) - - self.Insert(api, (key, value)) - - def RestrictionDict(self) -> dict[str, tuple[str, str]]: - """ - Get a copy of the restrictions dictionary. - - Returns: - dict: Copy of the restrictions mapping API names to (header_key, header_value) tuples - """ - return self._restrictions.copy() - - def Insert(self, api: str, restriction: tuple[str, str]): - """ - Add a restriction for a specific API. - - Args: - api: The API name (e.g., "inference", "model-repository") - restriction: Tuple of (header_key, header_value) for authentication - """ - self._restrictions[api] = restriction - - def IsRestricted(self, api: str) -> bool: - """ - Check if a specific API is restricted. - - Args: - api: The API name to check - - Returns: - bool: True if the API is restricted, False otherwise - """ - return api in self._restrictions - - -class APIRestrictionMiddleware(BaseHTTPMiddleware): - """ - Middleware to restrict API endpoint access based on allowed APIs configuration. - - This middleware intercepts HTTP requests and checks if they match any restricted - API endpoints. If a request matches a restricted endpoint, it validates the - authentication headers before allowing the request to proceed. - - Similar to Triton Server's endpoint access control feature. - """ - - def __init__(self, app, restricted_apis: RestrictedFeatures): - """ - Initialize the API restriction middleware. - - Args: - app: The FastAPI application instance - restricted_apis: RestrictedFeatures instance containing the restriction configuration - """ - super().__init__(app) - self.restricted_apis = restricted_apis - - def _get_auth_header(self, request: Request) -> tuple[str, str] | None: - request_method = request.method - request_path = request.url.path - - # Check each restricted API to see if the request matches - for ( - restricted_api, - auth_spec, - ) in self.restricted_apis.RestrictionDict().items(): - # Check each endpoint in the API - for restricted_endpoint in ENDPOINT_MAPPING[restricted_api]: - restricted_method, restricted_path = restricted_endpoint.split(" ") - - # Match both HTTP method and path prefix - if request_method == restricted_method and request_path.startswith( - restricted_path - ): - return auth_spec - return None - - async def dispatch(self, request: Request, call_next): - """ - Main middleware dispatch method that processes each incoming request. - - Args: - request: The incoming HTTP request - call_next: The next middleware/handler in the chain - - Returns: - Response: Either the next handler's response or a 401 authentication error - """ - # Check if the request matches any restricted patterns - auth_header = self._get_auth_header(request) - - # If request not restricted, proceed with the request - if not auth_header: - return await call_next(request) - - # Check authentication for the matching restricted endpoint - auth_result = self._check_authentication(request, auth_header) - if auth_result["valid"]: - # Authentication passed, allow request to proceed - return await call_next(request) - else: - # Authentication failed, return 401 error - return JSONResponse( - status_code=StatusCode.AUTHORIZATION_ERROR, - content={ - "error": { - "message": auth_result["message"], - "type": "authentication_error", - "code": "invalid_auth", - } - }, - ) - - def _check_authentication(self, request: Request, auth_header: tuple[str, str]): - """ - Check if the request contains valid authentication headers. - - Args: - request: The incoming HTTP request - auth_header: Tuple of (expected_header_key, expected_header_value) - - Returns: - dict: {"valid": bool, "message": str} - Authentication result and error message if invalid - """ - expected_key, expected_value = auth_header - - # Get the actual header value from the request - actual_value = request.headers.get(expected_key) - - # Validate the header value matches the expected value - if not actual_value or actual_value != expected_value: - return { - "valid": False, - "message": f"This API is restricted, expecting header '{expected_key}' with valid value", - } - - return {"valid": True} diff --git a/python/openai/openai_frontend/frontend/fastapi/middleware/request_size.py b/python/openai/openai_frontend/frontend/fastapi/middleware/request_size.py deleted file mode 100644 index 333a7968f4..0000000000 --- a/python/openai/openai_frontend/frontend/fastapi/middleware/request_size.py +++ /dev/null @@ -1,163 +0,0 @@ -# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -from fastapi.responses import JSONResponse -from starlette.types import ASGIApp, Message, Receive, Scope, Send -from utils.utils import StatusCode, validate_positive_int - - -async def _disconnect_receive() -> Message: - return {"type": "http.disconnect"} - - -class RequestSizeLimitMiddleware: - """ - Reject HTTP requests whose body exceeds ``http_max_input_size`` bytes. - First validation rejects on the Content-Length header before any body bytes are - read. Second validation streams the body chunks, counting bytes as they arrive, - and rejects as soon as the running total crosses the limit. Driving - receive() from the middleware protects every endpoint, including - handlers that never read the body. The buffered body is released the - moment the application consumes it, so the middleware contributes no - sustained memory overhead. - """ - - def __init__(self, app: ASGIApp, http_max_input_size: int) -> None: - self.app = app - self.http_max_input_size = validate_positive_int(http_max_input_size) - - async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: - if scope["type"] != "http": - await self.app(scope, receive, send) - return - - # Stage 1: reject on Content-Length before reading any body bytes. - for name, value in scope["headers"]: - if name != b"content-length": - continue - try: - content_length = int(value) - except ValueError: - await self._send_error( - scope, - send, - StatusCode.CLIENT_ERROR, - "invalid_content_length", - "Invalid Content-Length header: not an integer.", - ) - return - if content_length < 0: - await self._send_error( - scope, - send, - StatusCode.CLIENT_ERROR, - "invalid_content_length", - "Invalid Content-Length header: must be non-negative.", - ) - return - if content_length > self.http_max_input_size: - await self._send_error( - scope, - send, - StatusCode.CONTENT_TOO_LARGE, - "content_too_large", - self._oversized_request_message( - content_length, self.http_max_input_size - ), - ) - return - break - - # Stage 2: count chunks as they arrive, reject if total exceeds limit. - body_chunks: list[bytes] = [] - total = 0 - while True: - message = await receive() - if message["type"] != "http.request": - return - chunk = message.get("body", b"") - total += len(chunk) - if total > self.http_max_input_size: - await self._send_error( - scope, - send, - StatusCode.CONTENT_TOO_LARGE, - "content_too_large", - self._oversized_request_message(total, self.http_max_input_size), - ) - return - body_chunks.append(chunk) - if not message.get("more_body", False): - break - - # Assemble the buffered body and replay it to the app. - body_message: Message = { - "type": "http.request", - "body": b"".join(body_chunks), - "more_body": False, - } - del body_chunks - - async def replay_receive() -> Message: - nonlocal body_message - if body_message is not None: - # Drop the reference on hand-off so the body is freed while - # the app processes it, instead of being held by this closure. - message, body_message = body_message, None - return message - # Body already delivered — delegate to the original receive() so - # streaming responses can wait for the real client disconnect. - return await receive() - - await self.app(scope, replay_receive, send) - - @staticmethod - def _oversized_request_message(actual_bytes: int, max_bytes: int) -> str: - return ( - f"Request size of {actual_bytes} bytes exceeds the maximum allowed " - f"input size of {max_bytes} bytes. " - f"Use --http-max-input-size to increase the limit." - ) - - async def _send_error( - self, - scope: Scope, - send: Send, - status_code: int, - code: str, - message: str, - ) -> None: - response = JSONResponse( - status_code=status_code, - content={ - "error": { - "message": message, - "type": "invalid_request_error", - "code": code, - } - }, - ) - await response(scope, _disconnect_receive, send) diff --git a/python/openai/openai_frontend/frontend/fastapi/routers/chat.py b/python/openai/openai_frontend/frontend/fastapi/routers/chat.py index 49d1c5f23d..0f72047a5e 100644 --- a/python/openai/openai_frontend/frontend/fastapi/routers/chat.py +++ b/python/openai/openai_frontend/frontend/fastapi/routers/chat.py @@ -1,4 +1,4 @@ -# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -24,12 +24,9 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import traceback - from fastapi import APIRouter, HTTPException, Request from fastapi.responses import StreamingResponse from schemas.openai import CreateChatCompletionRequest, CreateChatCompletionResponse -from utils.utils import ClientError, ServerError, StatusCode router = APIRouter() @@ -45,20 +42,12 @@ async def create_chat_completion( Creates a chat completion for the provided messages and parameters. """ if not raw_request.app.engine: - raise HTTPException( - status_code=StatusCode.SERVER_ERROR, detail="No attached inference engine" - ) + raise HTTPException(status_code=500, detail="No attached inference engine") try: response = await raw_request.app.engine.chat(request) if request.stream: return StreamingResponse(response, media_type="text/event-stream") return response - except ClientError as e: - raise HTTPException(status_code=StatusCode.CLIENT_ERROR, detail=f"{e}") - except ServerError as e: - print(traceback.format_exc()) - raise HTTPException(status_code=StatusCode.SERVER_ERROR, detail=f"{e}") except Exception as e: - print(traceback.format_exc()) - raise HTTPException(status_code=StatusCode.SERVER_ERROR, detail=f"{e}") + raise HTTPException(status_code=400, detail=f"{e}") diff --git a/python/openai/openai_frontend/frontend/fastapi/routers/completions.py b/python/openai/openai_frontend/frontend/fastapi/routers/completions.py index 642bc117d0..ade89a47cc 100644 --- a/python/openai/openai_frontend/frontend/fastapi/routers/completions.py +++ b/python/openai/openai_frontend/frontend/fastapi/routers/completions.py @@ -1,4 +1,4 @@ -# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -24,12 +24,9 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import traceback - from fastapi import APIRouter, HTTPException, Request from fastapi.responses import StreamingResponse from schemas.openai import CreateCompletionRequest, CreateCompletionResponse -from utils.utils import ClientError, ServerError, StatusCode router = APIRouter() @@ -44,20 +41,12 @@ async def create_completion( Creates a completion for the provided prompt and parameters. """ if not raw_request.app.engine: - raise HTTPException( - status_code=StatusCode.SERVER_ERROR, detail="No attached inference engine" - ) + raise HTTPException(status_code=500, detail="No attached inference engine") try: response = await raw_request.app.engine.completion(request) if request.stream: return StreamingResponse(response, media_type="text/event-stream") return response - except ClientError as e: - raise HTTPException(status_code=StatusCode.CLIENT_ERROR, detail=f"{e}") - except ServerError as e: - print(traceback.format_exc()) - raise HTTPException(status_code=StatusCode.SERVER_ERROR, detail=f"{e}") except Exception as e: - print(traceback.format_exc()) - raise HTTPException(status_code=StatusCode.SERVER_ERROR, detail=f"{e}") + raise HTTPException(status_code=400, detail=f"{e}") diff --git a/python/openai/openai_frontend/frontend/fastapi/routers/embeddings.py b/python/openai/openai_frontend/frontend/fastapi/routers/embeddings.py deleted file mode 100644 index eb2ea5d9da..0000000000 --- a/python/openai/openai_frontend/frontend/fastapi/routers/embeddings.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import traceback - -from fastapi import APIRouter, HTTPException, Request -from fastapi.responses import StreamingResponse -from schemas.openai import CreateEmbeddingRequest, CreateEmbeddingResponse -from utils.utils import ClientError, ServerError, StatusCode - -router = APIRouter() - - -@router.post( - "/v1/embeddings", response_model=CreateEmbeddingResponse, tags=["Embeddings"] -) -async def create_embedding( - request: CreateEmbeddingRequest, raw_request: Request -) -> CreateEmbeddingResponse | StreamingResponse: - """ - Creates embedding for the provided input text. - """ - if not raw_request.app.engine: - raise HTTPException( - status_code=StatusCode.SERVER_ERROR, detail="No attached inference engine" - ) - - try: - response = await raw_request.app.engine.embedding(request) - return response - except ClientError as e: - raise HTTPException(status_code=StatusCode.CLIENT_ERROR, detail=f"{e}") - except ServerError as e: - print(traceback.format_exc()) - raise HTTPException(status_code=StatusCode.SERVER_ERROR, detail=f"{e}") - except Exception as e: - print(traceback.format_exc()) - raise HTTPException(status_code=StatusCode.SERVER_ERROR, detail=f"{e}") diff --git a/python/openai/openai_frontend/frontend/fastapi/routers/model_management.py b/python/openai/openai_frontend/frontend/fastapi/routers/model_management.py deleted file mode 100644 index 2ddecb8883..0000000000 --- a/python/openai/openai_frontend/frontend/fastapi/routers/model_management.py +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import traceback - -from fastapi import APIRouter, HTTPException, Request -from schemas.openai import Model -from utils.utils import ClientError, ServerError, StatusCode - -router = APIRouter() - - -@router.post( - "/v1/models/{model_name}/load", - response_model=Model, - tags=["Model Management"], -) -async def load_model(model_name: str, raw_request: Request) -> Model: - """ - Loads a model by name. Only available in EXPLICIT model control mode. - Blocks until the model is fully loaded and ready. - """ - if not raw_request.app.engine: - raise HTTPException( - status_code=StatusCode.SERVER_ERROR, - detail="No attached inference engine", - ) - - try: - return await raw_request.app.engine.load_model(model_name) - except ClientError as e: - raise HTTPException(status_code=StatusCode.CLIENT_ERROR, detail=f"{e}") - except ServerError as e: - print(traceback.format_exc()) - raise HTTPException(status_code=StatusCode.SERVER_ERROR, detail=f"{e}") - except Exception as e: - print(traceback.format_exc()) - raise HTTPException(status_code=StatusCode.SERVER_ERROR, detail=f"{e}") - - -@router.post( - "/v1/models/{model_name}/unload", - tags=["Model Management"], -) -async def unload_model(model_name: str, raw_request: Request) -> dict: - """ - Unloads a model by name. Only available in EXPLICIT model control mode. - Blocks until the model is fully unloaded. In-flight requests are allowed - to complete before the model is removed. - """ - if not raw_request.app.engine: - raise HTTPException( - status_code=StatusCode.SERVER_ERROR, - detail="No attached inference engine", - ) - - try: - await raw_request.app.engine.unload_model(model_name) - return {"status": "success", "model": model_name} - except ClientError as e: - raise HTTPException(status_code=StatusCode.CLIENT_ERROR, detail=f"{e}") - except ServerError as e: - print(traceback.format_exc()) - raise HTTPException(status_code=StatusCode.SERVER_ERROR, detail=f"{e}") - except Exception as e: - print(traceback.format_exc()) - raise HTTPException(status_code=StatusCode.SERVER_ERROR, detail=f"{e}") diff --git a/python/openai/openai_frontend/frontend/fastapi/routers/models.py b/python/openai/openai_frontend/frontend/fastapi/routers/models.py index 871e300276..ac2fa7fdc0 100644 --- a/python/openai/openai_frontend/frontend/fastapi/routers/models.py +++ b/python/openai/openai_frontend/frontend/fastapi/routers/models.py @@ -1,4 +1,4 @@ -# Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -28,7 +28,6 @@ from fastapi import APIRouter, HTTPException, Request from schemas.openai import ListModelsResponse, Model, ObjectType -from utils.utils import StatusCode router = APIRouter() @@ -36,28 +35,24 @@ @router.get("/v1/models", response_model=ListModelsResponse, tags=["Models"]) -async def list_models(request: Request) -> ListModelsResponse: +def list_models(request: Request) -> ListModelsResponse: """ Lists the currently available models, and provides basic information about each one such as the owner and availability. """ if not request.app.engine: - raise HTTPException( - status_code=StatusCode.SERVER_ERROR, detail="No attached inference engine" - ) + raise HTTPException(status_code=500, detail="No attached inference engine") models: List[Model] = request.app.engine.models() return ListModelsResponse(object=ObjectType.list, data=models) @router.get("/v1/models/{model_name}", response_model=Model, tags=["Models"]) -async def retrieve_model(request: Request, model_name: str) -> Model: +def retrieve_model(request: Request, model_name: str) -> Model: """ Retrieves a model instance, providing basic information about the model such as the owner and permissioning. """ if not request.app.engine: - raise HTTPException( - status_code=StatusCode.SERVER_ERROR, detail="No attached inference engine" - ) + raise HTTPException(status_code=500, detail="No attached inference engine") # TODO: Return model directly from engine instead of searching models models: List[Model] = request.app.engine.models() @@ -65,6 +60,4 @@ async def retrieve_model(request: Request, model_name: str) -> Model: if model.id == model_name: return model - raise HTTPException( - status_code=StatusCode.NOT_FOUND, detail=f"Unknown model: {model_name}" - ) + raise HTTPException(status_code=404, detail=f"Unknown model: {model_name}") diff --git a/python/openai/openai_frontend/frontend/fastapi/routers/observability.py b/python/openai/openai_frontend/frontend/fastapi/routers/observability.py index ca881c632e..b8040f56b7 100644 --- a/python/openai/openai_frontend/frontend/fastapi/routers/observability.py +++ b/python/openai/openai_frontend/frontend/fastapi/routers/observability.py @@ -1,4 +1,4 @@ -# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -26,7 +26,6 @@ from fastapi import APIRouter, HTTPException, Request from fastapi.responses import PlainTextResponse, Response -from utils.utils import StatusCode router = APIRouter() @@ -39,14 +38,12 @@ def metrics(request: Request) -> PlainTextResponse: @router.get("/health/ready", tags=["Utilities"]) def ready(request: Request) -> Response: if not request.app.engine: - raise HTTPException( - status_code=StatusCode.SERVER_ERROR, detail="No attached inference engine" - ) + raise HTTPException(status_code=500, detail="No attached inference engine") if not request.app.engine.ready(): raise HTTPException( - status_code=StatusCode.CLIENT_ERROR, + status_code=400, detail="Attached inference engine is not ready for inference requests.", ) - return Response(status_code=StatusCode.SUCCESS) + return Response(status_code=200) diff --git a/python/openai/openai_frontend/frontend/fastapi_frontend.py b/python/openai/openai_frontend/frontend/fastapi_frontend.py index 752befd8bc..adee4cbab3 100644 --- a/python/openai/openai_frontend/frontend/fastapi_frontend.py +++ b/python/openai/openai_frontend/frontend/fastapi_frontend.py @@ -1,4 +1,4 @@ -# Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -30,21 +30,8 @@ from engine.triton_engine import TritonLLMEngine from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from frontend.fastapi.middleware.api_restriction import ( - APIRestrictionMiddleware, - RestrictedFeatures, -) -from frontend.fastapi.middleware.request_size import RequestSizeLimitMiddleware -from frontend.fastapi.routers import ( - chat, - completions, - embeddings, - model_management, - models, - observability, -) +from frontend.fastapi.routers import chat, completions, models, observability from frontend.frontend import OpenAIFrontend -from utils.utils import HTTP_DEFAULT_MAX_INPUT_SIZE class FastApiFrontend(OpenAIFrontend): @@ -54,19 +41,10 @@ def __init__( host: str = "localhost", port: int = 8000, log_level: str = "info", - restricted_apis: list = None, - http_max_input_size: int = HTTP_DEFAULT_MAX_INPUT_SIZE, ): self.host: str = host self.port: int = port self.log_level: str = log_level - self.http_max_input_size: int = http_max_input_size - if restricted_apis: - self.restricted_apis: RestrictedFeatures = RestrictedFeatures( - restricted_apis - ) - else: - self.restricted_apis: RestrictedFeatures = None self.stopped: bool = False self.app = self._create_app() @@ -106,16 +84,11 @@ def _create_app(self): app.include_router(observability.router) app.include_router(models.router) - app.include_router(model_management.router) app.include_router(completions.router) app.include_router(chat.router) - app.include_router(embeddings.router) # NOTE: For debugging purposes, should generally be restricted or removed self._add_cors_middleware(app) - if self.restricted_apis != None: - self._add_api_restriction_middleware(app) - self._add_request_size_limit_middleware(app) return app @@ -134,18 +107,3 @@ def _add_cors_middleware(self, app: FastAPI): allow_methods=["*"], allow_headers=["*"], ) - - def _add_api_restriction_middleware(self, app: FastAPI): - app.add_middleware( - APIRestrictionMiddleware, restricted_apis=self.restricted_apis - ) - print( - f"[INFO] API restrictions enabled. Restricted API endpoints: {self.restricted_apis.RestrictionDict()}" - ) - - def _add_request_size_limit_middleware(self, app: FastAPI): - app.add_middleware( - RequestSizeLimitMiddleware, - http_max_input_size=self.http_max_input_size, - ) - print(f"[INFO] HTTP request size limit set to {self.http_max_input_size} bytes") diff --git a/python/openai/openai_frontend/main.py b/python/openai/openai_frontend/main.py index cf9228b9fc..5a4e2368a0 100755 --- a/python/openai/openai_frontend/main.py +++ b/python/openai/openai_frontend/main.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -28,13 +28,11 @@ import argparse import signal -import sys from functools import partial import tritonserver from engine.triton_engine import TritonLLMEngine from frontend.fastapi_frontend import FastApiFrontend -from utils.utils import HTTP_DEFAULT_MAX_INPUT_SIZE, validate_positive_int def signal_handler( @@ -110,12 +108,6 @@ def parse_args(): choices=["vllm", "tensorrtllm"], help="Manual override of Triton backend request format (inputs/output names) to use for inference", ) - triton_group.add_argument( - "--lora-separator", - type=str, - default=None, - help="LoRA name selection may be appended to the model name following this separator if the separator is provided", - ) triton_group.add_argument( "--tritonserver-log-verbose-level", type=int, @@ -128,54 +120,6 @@ def parse_args(): default="0.0.0.0", help="Address/host of frontends (default: '0.0.0.0')", ) - triton_group.add_argument( - "--tool-call-parser", - type=str, - default=None, - help="Specify the parser for handling tool calling related response text. Options include: 'llama3' and 'mistral'.", - ) - # Allows the user to try a different chat template to craft better prompts and receive more targeted tool-calling responses from the model. - # Some Mistral models have a separate chat template file, in addition to the tokenizer_config.json, - # such as mistralai/Mistral-Small-3.1-24B-Instruct-2503. - # This can serve as a workaround for those models. - triton_group.add_argument( - "--chat-template", - type=str, - default=None, - help="The path to the custom Jinja chat template file. This is useful if you'd like to use a different chat template than the one provided by the model.", - ) - - triton_group.add_argument( - "--default-max-tokens", - type=int, - default=16, - help="The default maximum number of tokens to generate if not specified in the request. The default is 16.", - ) - triton_group.add_argument( - "--model-control-mode", - type=str, - default="none", - choices=["none", "explicit"], - help="Specify the mode for model management. Options are 'none', and 'explicit'. " - "The default is 'none'. For 'none', the server will load all models in the model " - "repository at startup and will not make any changes to the loaded " - "models after that. For 'explicit', model load and unload are initiated by using the " - "model control APIs, and only models specified with --load-model will " - "be loaded at startup.", - ) - triton_group.add_argument( - "--load-model", - type=str, - action="append", - default=None, - help="Name of the model to be loaded on server startup. It may be specified " - "multiple times to add multiple models. To load ALL models at startup, " - "specify '*' as the model name with --load-model=* as the ONLY " - "--load-model argument, this does not imply any pattern matching. " - "Specifying --load-model=* in conjunction with another --load-model " - "argument will result in error. Note that this option will only take " - "effect if --model-control-mode is set to 'explicit'.", - ) # OpenAI-Compatible Frontend (FastAPI) openai_group = parser.add_argument_group("Triton OpenAI-Compatible Frontend") @@ -189,23 +133,6 @@ def parse_args(): choices=["debug", "info", "warning", "error", "critical", "trace"], help="log level for uvicorn", ) - openai_group.add_argument( - "--openai-restricted-api", - type=str, - default=None, - nargs=3, - metavar=("APIs", "Restricted Key", "Restricted Value"), - action="append", - help="Restrict access to specific OpenAI API endpoints. Format: ',,... ' (e.g., 'inference,model-repository admin-key admin-value'). If not specified, all endpoints are allowed.", - ) - openai_group.add_argument( - "--http-max-input-size", - type=validate_positive_int, - default=HTTP_DEFAULT_MAX_INPUT_SIZE, - help=f"Maximum allowed HTTP request input size in bytes for the OpenAI " - f"frontend (default: {HTTP_DEFAULT_MAX_INPUT_SIZE}, i.e. 64 MiB). " - "Requests exceeding this limit will be rejected.", - ) # KServe Predict v2 Frontend kserve_group = parser.add_argument_group("Triton KServe Frontend") @@ -234,25 +161,8 @@ def main(): args = parse_args() # Initialize a Triton Inference Server pointing at LLM models - model_control_mode = ( - tritonserver.ModelControlMode.EXPLICIT - if args.model_control_mode == "explicit" - else tritonserver.ModelControlMode.NONE - ) - - load_models = args.load_model or [] - if load_models and model_control_mode != tritonserver.ModelControlMode.EXPLICIT: - print( - "Error: Use of '--load-model' requires setting " - "'--model-control-mode=explicit' as well.", - file=sys.stderr, - ) - sys.exit(1) - server: tritonserver.Server = tritonserver.Server( model_repository=args.model_repository, - model_control_mode=model_control_mode, - startup_models=load_models, log_verbose=args.tritonserver_log_verbose_level, log_info=True, log_warn=True, @@ -261,31 +171,16 @@ def main(): # Wrap Triton Inference Server in an interface-conforming "LLMEngine" engine: TritonLLMEngine = TritonLLMEngine( - server=server, - tokenizer=args.tokenizer, - backend=args.backend, - lora_separator=args.lora_separator, - tool_call_parser=args.tool_call_parser, - chat_template=args.chat_template, - default_max_tokens=args.default_max_tokens, + server=server, tokenizer=args.tokenizer, backend=args.backend ) # Attach TritonLLMEngine as the backbone for inference and model management - try: - openai_frontend: FastApiFrontend = FastApiFrontend( - engine=engine, - host=args.host, - port=args.openai_port, - log_level=args.uvicorn_log_level, - restricted_apis=args.openai_restricted_api, - http_max_input_size=args.http_max_input_size, - ) - except ValueError as e: - print( - f"[ERROR] Failed to initialize FastAPI frontend: {e}", - file=sys.stderr, - ) - sys.exit(1) + openai_frontend: FastApiFrontend = FastApiFrontend( + engine=engine, + host=args.host, + port=args.openai_port, + log_level=args.uvicorn_log_level, + ) # Optionally expose Triton KServe HTTP/GRPC Frontends kserve_http, kserve_grpc = None, None diff --git a/python/openai/openai_frontend/schemas/openai.py b/python/openai/openai_frontend/schemas/openai.py index 57fb7b1017..8b5545d910 100644 --- a/python/openai/openai_frontend/schemas/openai.py +++ b/python/openai/openai_frontend/schemas/openai.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -31,7 +31,7 @@ from __future__ import annotations from enum import Enum -from typing import Any, Dict, List, Literal, Optional, Union +from typing import Any, Dict, List, Optional, Union from pydantic import AnyUrl, BaseModel, ConfigDict, Field, RootModel, confloat, conint @@ -103,7 +103,7 @@ class CreateCompletionRequest(BaseModel): description="Include the log probabilities on the `logprobs` most likely output tokens, as well the chosen tokens. For example, if `logprobs` is 5, the API will return a list of the 5 most likely tokens. The API will always return the `logprob` of the sampled token, so there may be up to `logprobs+1` elements in the response.\n\nThe maximum value for `logprobs` is 5.\n", ) max_tokens: Optional[conint(ge=0)] = Field( - None, + 16, description="The maximum number of [tokens](/tokenizer) that can be generated in the completion.\n\nThe token count of your prompt plus `max_tokens` cannot exceed the model's context length. [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) for counting tokens.\n", examples=[16], ) @@ -133,10 +133,6 @@ class CreateCompletionRequest(BaseModel): False, description="Whether to stream back partial progress. If set, tokens will be sent as data-only [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) as they become available, with the stream terminated by a `data: [DONE]` message. [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions).\n", ) - stream_options: Optional[StreamOptions] = Field( - None, - description="Options for streaming responses. Only use when `stream` is set to `true`.", - ) suffix: Optional[str] = Field( None, description="The suffix that comes after a completion of inserted text.\n\nThis parameter is only supported for `gpt-3.5-turbo-instruct`.\n", @@ -324,6 +320,10 @@ class ChatCompletionFunctionCallOption(BaseModel): name: str = Field(..., description="The name of the function to call.") +class Type2(Enum): + function = "function" + + class FunctionObject(BaseModel): description: Optional[str] = Field( None, @@ -347,7 +347,7 @@ class Function(BaseModel): class ChatCompletionNamedToolChoice(BaseModel): - type: str = Field( + type: Type2 = Field( ..., description="The type of the tool. Currently, only `function` is supported.", ) @@ -364,7 +364,7 @@ class Function1(BaseModel): class ChatCompletionMessageToolCall(BaseModel): id: str = Field(..., description="The ID of the tool call.") - type: str = Field( + type: Type2 = Field( ..., description="The type of the tool. Currently, only `function` is supported.", ) @@ -382,7 +382,7 @@ class Function2(BaseModel): class ChatCompletionMessageToolCallChunk(BaseModel): index: int id: Optional[str] = Field(None, description="The ID of the tool call.") - type: Optional[str] = Field( + type: Optional[Type2] = Field( None, description="The type of the tool. Currently, only `function` is supported.", ) @@ -471,13 +471,6 @@ class ResponseFormat(BaseModel): ) -class StreamOptions(BaseModel): - include_usage: Optional[bool] = Field( - False, - description="If enabled, an additional chunk is sent before the `data: [DONE]` message. That chunk’s `usage` field reports the total token usage for the request and its `choices` array is always empty. All other chunks include a `usage` field with a null value.", - ) - - class FunctionCall3(Enum): none = "none" auto = "auto" @@ -530,16 +523,24 @@ class ChatCompletionTokenLogprob(BaseModel): ) -class ChatCompletionLogprobs(BaseModel): +class Logprobs2(BaseModel): content: List[ChatCompletionTokenLogprob] = Field( ..., description="A list of message content tokens with log probability information.", ) +class ChatCompletionFinishReason(Enum): + stop = "stop" + length = "length" + tool_calls = "tool_calls" + content_filter = "content_filter" + function_call = "function_call" + + class ChatCompletionStreamingResponseChoice(BaseModel): delta: ChatCompletionStreamResponseDelta - logprobs: Optional[ChatCompletionLogprobs] = Field( + logprobs: Optional[Logprobs2] = Field( None, description="Log probability information for the choice." ) finish_reason: ChatCompletionFinishReason | None = Field( @@ -576,7 +577,6 @@ class CreateChatCompletionStreamResponse(BaseModel): object: Object4 = Field( ..., description="The object type, which is always `chat.completion.chunk`." ) - usage: Optional[CompletionUsage] = None class CreateChatCompletionImageResponse(BaseModel): @@ -601,7 +601,10 @@ class Model(BaseModel): owned_by: str = Field(..., description="The organization that owns the model.") -class BaseUsage(BaseModel): +class CompletionUsage(BaseModel): + completion_tokens: int = Field( + ..., description="Number of tokens in the generated completion." + ) prompt_tokens: int = Field(..., description="Number of tokens in the prompt.") total_tokens: int = Field( ..., @@ -609,16 +612,6 @@ class BaseUsage(BaseModel): ) -class EmbeddingUsage(BaseUsage): - pass - - -class CompletionUsage(BaseUsage): - completion_tokens: int = Field( - ..., description="Number of tokens in the generated completion." - ) - - class Event(Enum): error = "error" @@ -690,7 +683,7 @@ class ChatCompletionRequestUserMessage(BaseModel): class ChatCompletionTool(BaseModel): - type: str = Field( + type: Type2 = Field( ..., description="The type of the tool. Currently, only `function` is supported.", ) @@ -730,7 +723,7 @@ class ChatCompletionChoice(BaseModel): ..., description="The index of the choice in the list of choices." ) message: ChatCompletionResponseMessage - logprobs: ChatCompletionLogprobs | None = Field( + logprobs: Logprobs2 | None = Field( ..., description="Log probability information for the choice." ) @@ -861,14 +854,10 @@ class CreateChatCompletionRequest(BaseModel): None, description="An integer between 0 and 20 specifying the number of most likely tokens to return at each token position, each with an associated log probability. `logprobs` must be set to `true` if this parameter is used.", ) - max_completion_tokens: Optional[conint(ge=0)] = Field( - None, - description="The maximum number of [tokens](/tokenizer) that can be generated in the chat completion.\n\nThe total length of input tokens and generated tokens is limited by the model's context length. [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) for counting tokens.\n", - ) - # TODO: Remove support for max_tokens field in the future: https://platform.openai.com/docs/api-reference/chat/create#chat-create-max_completion_tokens + # TODO: Consider new max_completion_tokens field in the future: https://platform.openai.com/docs/api-reference/chat/create#chat-create-max_completion_tokens max_tokens: Optional[conint(ge=0)] = Field( - None, - description="DEPRECATED: Use `max_completion_tokens` instead. The maximum number of [tokens](/tokenizer) that can be generated in the chat completion.\n\nThe total length of input tokens and generated tokens is limited by the model's context length. [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) for counting tokens.\n", + 16, + description="The maximum number of [tokens](/tokenizer) that can be generated in the chat completion.\n\nThe total length of input tokens and generated tokens is limited by the model's context length. [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) for counting tokens.\n", ) # TODO: Extension, flesh out description and defaults min_tokens: Optional[conint(ge=0)] = Field( @@ -886,7 +875,7 @@ class CreateChatCompletionRequest(BaseModel): ) response_format: Optional[ResponseFormat] = Field( None, - description='An object specifying the format that the model must output. Compatible with [GPT-4 Turbo](/docs/models/gpt-4-and-gpt-4-turbo) and all GPT-3.5 Turbo models newer than `gpt-3.5-turbo-1106`.\n\nSetting to `{ "type": "json_object" }` enables JSON mode, which guarantees the message the model generates is valid JSON.\n\n**Important:** when using JSON mode, you **must** also instruct the model to produce JSON yourself via a system or user message. Without this, the model may generate an unending stream of whitespace until the generation reaches the token limit, resulting in a long-running and seemingly "stuck" request. Also note that the message content may be partially cut off if `finish_reason="length"`, which indicates the generation exceeded `max_completion_tokens` or the conversation exceeded the max context length.\n', + description='An object specifying the format that the model must output. Compatible with [GPT-4 Turbo](/docs/models/gpt-4-and-gpt-4-turbo) and all GPT-3.5 Turbo models newer than `gpt-3.5-turbo-1106`.\n\nSetting to `{ "type": "json_object" }` enables JSON mode, which guarantees the message the model generates is valid JSON.\n\n**Important:** when using JSON mode, you **must** also instruct the model to produce JSON yourself via a system or user message. Without this, the model may generate an unending stream of whitespace until the generation reaches the token limit, resulting in a long-running and seemingly "stuck" request. Also note that the message content may be partially cut off if `finish_reason="length"`, which indicates the generation exceeded `max_tokens` or the conversation exceeded the max context length.\n', ) seed: Optional[conint(ge=-9223372036854775808, le=9223372036854775807)] = Field( None, @@ -900,10 +889,6 @@ class CreateChatCompletionRequest(BaseModel): False, description="If set, partial message deltas will be sent, like in ChatGPT. Tokens will be sent as data-only [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) as they become available, with the stream terminated by a `data: [DONE]` message. [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions).\n", ) - stream_options: Optional[StreamOptions] = Field( - None, - description="Options for streaming responses. Only use when `stream` is set to `true`.", - ) temperature: Optional[confloat(ge=0.0, le=2.0)] = Field( 0.7, description="What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\n\nWe generally recommend altering this or `top_p` but not both.\n", @@ -947,69 +932,3 @@ class ObjectType: text_completion = Object1.text_completion chat_completion_chunk = Object4.chat_completion_chunk chat_completion = Object2.chat_completion - - -class EmbeddingObject(BaseModel): - model_config: ConfigDict = ConfigDict(extra="forbid") - - object: Literal["embedding"] = Field( - description="The object type, which is always 'embedding'.", - ) - embedding: Union[List[float], str] = Field( - ..., - description="The embedding vector, which is a list of floats or a base64-encoded string.", - ) - index: int = Field( - ..., - description="The index of the embedding in the list of embeddings.", - ) - - -class CreateEmbeddingRequest(BaseModel): - # Explicitly return errors for unknown fields. - model_config: ConfigDict = ConfigDict(extra="forbid") - - input: Union[str, List[int]] = Field( - ..., - description="Input text to embed, encoded as a string or array of tokens. To embed multiple inputs in a single request, pass an array of strings or array of token arrays.", - min_length=1, - examples=["The food was delicious and the waiter..."], - ) - model: Union[str, Model2] = Field( - ..., - description="ID of the model to use. See the [model endpoint compatibility](/docs/models/model-endpoint-compatibility) table for details on which models work with the Chat API.", - examples=["text-embedding-ada-002"], - ) - dimensions: Optional[int] = Field( - None, - description="The number of dimensions the resulting output embeddings should have. Only supported in text-embedding-3 and later models.", - ) - encoding_format: Optional[Literal["float", "base64"]] = Field( - "float", - description="The format to return the embeddings in.", - ) - user: Optional[str] = Field( - None, - description="A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](/docs/guides/safety-best-practices/end-user-ids).\n", - examples=["user-1234"], - ) - - -class CreateEmbeddingResponse(BaseModel): - model_config: ConfigDict = ConfigDict(extra="forbid") - - object: Literal["list"] = Field( - description="The object type, which is always 'list'.", - ) - data: List[EmbeddingObject] = Field( - ..., - description="The list of embeddings.", - ) - model: Union[str, Model2] = Field( - ..., - description="The model used to generate the embeddings.", - ) - usage: Optional[EmbeddingUsage] = Field( - ..., - description="The usage for the request.", - ) diff --git a/python/openai/openai_frontend/utils/utils.py b/python/openai/openai_frontend/utils/utils.py deleted file mode 100644 index 86b4d921e4..0000000000 --- a/python/openai/openai_frontend/utils/utils.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -from enum import IntEnum - -# Default value for the --http-max-input-size CLI flag (64 MiB). -# Same as HTTP_DEFAULT_MAX_INPUT_SIZE in src/common.h. -HTTP_DEFAULT_MAX_INPUT_SIZE: int = 1 << 26 - - -class ServerError(Exception): - """Exception raised for server errors.""" - - pass - - -class ClientError(Exception): - """Exception raised for client errors.""" - - pass - - -class StatusCode(IntEnum): - SUCCESS = 200 - CLIENT_ERROR = 400 - AUTHORIZATION_ERROR = 401 - NOT_FOUND = 404 - CONTENT_TOO_LARGE = 413 - SERVER_ERROR = 500 - - -def validate_positive_int(value: object) -> int: - try: - ivalue = int(value) - except (TypeError, ValueError): - raise ValueError(f"value is not an integer, got {value!r}") - if ivalue <= 0: - raise ValueError(f"value must be greater than 0, got {value!r}") - return ivalue diff --git a/python/openai/requirements.txt b/python/openai/requirements.txt index 4b4f631f9b..ac84944631 100644 --- a/python/openai/requirements.txt +++ b/python/openai/requirements.txt @@ -1,4 +1,4 @@ -# Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -25,16 +25,11 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # FastAPI Application -fastapi==0.121.2 +fastapi==0.115.6 # Fix httpx version to avoid bug in openai library: # https://community.openai.com/t/error-with-openai-1-56-0-client-init-got-an-unexpected-keyword-argument-proxies/1040332/3 httpx==0.27.2 -openai==1.107.3 -partial-json-parser # used for parsing partial JSON outputs - -# FIXME [TRI-641]: The latest stable version of scipy is 1.17.0 which caused segfault during tests. See TRI-620. -scipy==1.16.3 -# Minimum starlette version needed to address CVE(s): +openai==1.60.0 +# Minimum starlette version needed to address CVE: # https://github.com/advisories/GHSA-f96h-pmfr-66vw -# https://github.com/advisories/GHSA-7f5h-v6xp-fcq8 -starlette>=0.49.1 +starlette>=0.40.0 diff --git a/python/openai/tests/conftest.py b/python/openai/tests/conftest.py index a1195000ed..9ea9a5634e 100644 --- a/python/openai/tests/conftest.py +++ b/python/openai/tests/conftest.py @@ -1,4 +1,4 @@ -# Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -32,25 +32,14 @@ from tests.utils import OpenAIServer, setup_fastapi_app, setup_server -def pytest_configure(config): - """Register custom markers.""" - config.addinivalue_line( - "markers", "openai: mark test to run with OpenAI server (subprocess)" - ) - config.addinivalue_line("markers", "asyncio: mark test as an asyncio test") - - ### TEST ENVIRONMENT SETUP ### -def infer_test_environment(tool_call_parser): +def infer_test_environment(): # Infer the test environment for simplicity in local dev/testing. try: import vllm as _ backend = "vllm" - if tool_call_parser == "mistral": - model = "mistral-nemo-instruct-2407" - else: - model = "llama-3.1-8b-instruct" + model = "llama-3.1-8b-instruct" return backend, model except ImportError: print("No vllm installation found.") @@ -67,88 +56,41 @@ def infer_test_environment(tool_call_parser): raise Exception("Unknown test environment") -def infer_test_model_repository(backend, tool_call_parser): - if tool_call_parser == "mistral": - model_repository = str(Path(__file__).parent / f"{backend}_mistral_models") - else: - model_repository = str(Path(__file__).parent / f"{backend}_models") +def infer_test_model_repository(backend): + model_repository = str(Path(__file__).parent / f"{backend}_models") return model_repository -### FIXTURES - Refactored from global variables ### - - -@pytest.fixture(scope="session") -def tool_call_parser(): - return os.environ.get("TEST_TOOL_CALL_PARSER", "llama3") - - -@pytest.fixture(scope="session") -def backend(tool_call_parser): - env_backend = os.environ.get("TEST_BACKEND") - env_model = os.environ.get("TEST_MODEL") - - if not env_backend or not env_model: - inferred_backend, _ = infer_test_environment(tool_call_parser) - return inferred_backend - return env_backend - - -@pytest.fixture(scope="session") -def model(tool_call_parser): - env_model = os.environ.get("TEST_MODEL") - - if not env_model: - _, inferred_model = infer_test_environment(tool_call_parser) - return inferred_model - return env_model - - -@pytest.fixture(scope="session") -def model_repository(backend, tool_call_parser): - env_repo = os.environ.get("TEST_MODEL_REPOSITORY") - - if env_repo: - return env_repo - return infer_test_model_repository(backend, tool_call_parser) - - -@pytest.fixture(scope="session") -def tokenizer_model(): - return os.environ.get("TEST_TOKENIZER", "meta-llama/Meta-Llama-3.1-8B-Instruct") - - -@pytest.fixture(scope="session") -def prompt(): - return "What is machine learning?" - +# TODO: Refactor away from global variables +TEST_MODEL = os.environ.get("TEST_MODEL") +TEST_BACKEND = os.environ.get("TEST_BACKEND") +TEST_MODEL_REPOSITORY = os.environ.get("TEST_MODEL_REPOSITORY") -@pytest.fixture(scope="session") -def messages(prompt): - return [{"role": "user", "content": prompt}] +TEST_TOKENIZER = os.environ.get( + "TEST_TOKENIZER", "meta-llama/Meta-Llama-3.1-8B-Instruct" +) +TEST_PROMPT = "What is machine learning?" +TEST_MESSAGES = [{"role": "user", "content": TEST_PROMPT}] +if not TEST_BACKEND or not TEST_MODEL: + TEST_BACKEND, TEST_MODEL = infer_test_environment() -@pytest.fixture(scope="session") -def input(prompt): - return prompt +if not TEST_MODEL_REPOSITORY: + TEST_MODEL_REPOSITORY = infer_test_model_repository(TEST_BACKEND) # NOTE: OpenAI client requires actual server running, and won't work # with the FastAPI TestClient. Run the server at module scope to run # only once for all the tests below. @pytest.fixture(scope="module") -def server( - model_repository: str, tokenizer_model: str, backend: str, tool_call_parser: str -): +def server(): args = [ "--model-repository", - model_repository, + TEST_MODEL_REPOSITORY, "--tokenizer", - tokenizer_model, + TEST_TOKENIZER, "--backend", - backend, - "--tool-call-parser", - tool_call_parser, + TEST_BACKEND, ] # TODO: Incorporate kserve frontend binding smoke tests to catch any # breakage with default values or slight cli arg variations @@ -164,36 +106,42 @@ def server( # with arbitrary clients - you must use the TestClient returned to interact with # the "server" when "starting the server" via TestClient. @pytest.fixture(scope="class") -def fastapi_client_class_scope( - model_repository: str, tokenizer_model: str, backend: str -): - server = setup_server(model_repository=model_repository) - app = setup_fastapi_app(tokenizer=tokenizer_model, server=server, backend=backend) +def fastapi_client_class_scope(): + server = setup_server(model_repository=TEST_MODEL_REPOSITORY) + app = setup_fastapi_app( + tokenizer=TEST_TOKENIZER, server=server, backend=TEST_BACKEND + ) with TestClient(app) as test_client: yield test_client server.stop() -# FIXME: In TRTLLM tests, the in-process Triton server for the FastAPI app -# does not automatically release GPU memory, even after calling stop(). -# The memory is only released when the entire pytest process exits. -# -# As a result, when the OpenAI server starts another Triton server as a subprocess, -# there may not be enough GPU memory available to launch a new model instance. -# -# This is a workaround to ensure that tests using the OpenAI server run first. -# Once the OpenAI server subprocess is terminated, tests using the FastAPI app can safely run. -def pytest_collection_modifyitems(session, config, items): - def get_priority(item): - cls = item.cls - if cls: - if getattr(cls, "pytestmark", None): - for mark in cls.pytestmark: - if mark.name == "openai": - return 0 - elif mark.name == "fastapi": - return 1 - return 2 # unmarked tests last - - items.sort(key=get_priority) +@pytest.fixture(scope="module") +def model_repository(): + return TEST_MODEL_REPOSITORY + + +@pytest.fixture(scope="module") +def model(): + return TEST_MODEL + + +@pytest.fixture(scope="module") +def backend(): + return TEST_BACKEND + + +@pytest.fixture(scope="module") +def tokenizer_model(): + return TEST_TOKENIZER + + +@pytest.fixture(scope="module") +def prompt(): + return TEST_PROMPT + + +@pytest.fixture(scope="module") +def messages(): + return TEST_MESSAGES diff --git a/python/openai/tests/test_chat_completions.py b/python/openai/tests/test_chat_completions.py index eee18bf354..401601c526 100644 --- a/python/openai/tests/test_chat_completions.py +++ b/python/openai/tests/test_chat_completions.py @@ -1,4 +1,4 @@ -# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -50,9 +50,8 @@ def test_chat_completions_defaults(self, client, model: str, messages: List[dict message = response.json()["choices"][0]["message"] assert message["content"].strip() assert message["role"] == "assistant" - - usage = response.json().get("usage") - assert usage is not None + # "usage" currently not supported + assert not response.json()["usage"] def test_chat_completions_system_prompt(self, client, model: str): # NOTE: Currently just sanity check that there are no issues when a @@ -123,7 +122,6 @@ def test_chat_completions_user_prompt_dict(self, client, model: str): [ ("temperature", 0.7), ("max_tokens", 10), - ("max_completion_tokens", 10), ("top_p", 0.9), ("frequency_penalty", 0.5), ("presence_penalty", 0.2), @@ -154,22 +152,12 @@ def test_chat_completions_sampling_parameters( ) # FIXME: Add support and remove this check - unsupported_parameters = ["logit_bias"] + unsupported_parameters = ["logprobs", "logit_bias"] if param_key in unsupported_parameters: - assert response.status_code == 400 - assert response.json()["detail"] == "logit bias is not currently supported" - return - - # TRT-LLM backend doesn't support logprobs - if ( - param_key == "logprobs" - and param_value is True - and model == "tensorrt_llm_bls" - ): assert response.status_code == 400 assert ( - "logprobs are currently available only for the vLLM backend" - in response.json()["detail"] + response.json()["detail"] + == "logit bias and log probs not currently supported" ) return @@ -183,7 +171,6 @@ def test_chat_completions_sampling_parameters( ("temperature", 2.1), ("temperature", -0.1), ("max_tokens", -1), - ("max_completion_tokens", -1), ("top_p", 1.1), ("frequency_penalty", 3), ("frequency_penalty", -3), @@ -211,21 +198,14 @@ def test_chat_completions_invalid_sampling_parameters( assert response.status_code == 422 # Simple tests to verify max_tokens roughly behaves as expected - @pytest.mark.parametrize( - "max_tokens_key", - [ - "max_tokens", - "max_completion_tokens", - ], - ) def test_chat_completions_max_tokens( - self, client, max_tokens_key, model: str, messages: List[dict] + self, client, model: str, messages: List[dict] ): responses = [] - payload = {"model": model, "messages": messages} + payload = {"model": model, "messages": messages, "max_tokens": 1} - # Send two requests with max_tokens/max_completion_tokens = 1 to check their similarity - payload[max_tokens_key] = 1 + # Send two requests with max_tokens = 1 to check their similarity + payload["max_tokens"] = 1 responses.append( client.post( "/v1/chat/completions", @@ -238,8 +218,8 @@ def test_chat_completions_max_tokens( json=payload, ) ) - # Send one requests with larger max_tokens/max_completion_tokens to check its dis-similarity - payload[max_tokens_key] = 100 + # Send one requests with larger max_tokens to check its dis-similarity + payload["max_tokens"] = 100 responses.append( client.post( "/v1/chat/completions", @@ -264,30 +244,6 @@ def test_chat_completions_max_tokens( assert len(response1_text) == len(response2_text) == 1 assert len(response3_text) > len(response1_text) - def test_chat_completions_max_completion_tokens_precedence( - self, client, model: str, messages: List[dict] - ): - payload = { - "model": model, - "messages": messages, - "max_tokens": 50, # Higher value for max_tokens - "max_completion_tokens": 1, # Lower, expected to take precedence - } - - response = client.post( - "/v1/chat/completions", - json=payload, - ) - - print("Response:", response.json()) - assert response.status_code == 200 - - response_text_words = ( - response.json()["choices"][0]["message"]["content"].strip().split() - ) - # Check if the number of words is around max_completion_tokens - assert len(response_text_words) == 1 - @pytest.mark.parametrize( "temperature", [0.0, 1.0], @@ -303,7 +259,7 @@ def test_chat_completions_temperature_vllm( payload = { "model": model, "messages": messages, - "max_completion_tokens": 256, + "max_tokens": 256, "temperature": temperature, } @@ -364,7 +320,7 @@ def test_chat_completions_temperature_tensorrtllm( "model": model, "messages": messages, # Increase token length to allow more room for variability - "max_completion_tokens": 200, + "max_tokens": 200, "temperature": 0.0, # TRT-LLM requires certain settings of `top_k` / `top_p` to # respect changes in `temperature` @@ -419,7 +375,7 @@ def test_chat_completions_seed(self, client, model: str, messages: List[dict]): "model": model, "messages": messages, # Increase token length to allow more room for variability - "max_completion_tokens": 200, + "max_tokens": 200, "seed": 1, } payload2 = copy.deepcopy(payload1) @@ -533,156 +489,16 @@ def test_request_n_choices(self): pass @pytest.mark.skip(reason="Not Implemented Yet") - def test_request_logit_bias(self): + def test_request_logprobs(self): pass - def test_usage_response(self, client, model: str, messages: List[dict]): - response = client.post( - "/v1/chat/completions", - json={"model": model, "messages": messages}, - ) - - assert response.status_code == 200 - usage = response.json().get("usage") - assert usage is not None - assert isinstance(usage["prompt_tokens"], int) - assert isinstance(usage["completion_tokens"], int) - assert isinstance(usage["total_tokens"], int) - assert usage["prompt_tokens"] > 0 - assert usage["completion_tokens"] > 0 - assert ( - usage["total_tokens"] == usage["prompt_tokens"] + usage["completion_tokens"] - ) - - def test_chat_completions_logprobs( - self, client, backend: str, model: str, messages: List[dict] - ): - """Test logprobs parameter for chat completions.""" - response = client.post( - "/v1/chat/completions", - json={ - "model": model, - "messages": messages, - "logprobs": True, - "top_logprobs": 2, - "max_tokens": 10, - }, - ) - - # Non-vLLM backends should raise an error - if backend != "vllm": - assert response.status_code == 400 - assert ( - "logprobs are currently available only for the vLLM backend" - in response.json()["detail"] - ) - return - - assert response.status_code == 200 - response_json = response.json() - - # Check that logprobs are present in the response - choice = response_json["choices"][0] - assert "logprobs" in choice - logprobs = choice["logprobs"] - - assert logprobs is not None - assert "content" in logprobs - content = logprobs["content"] - assert isinstance(content, list) - assert len(content) > 0 - - # Validate structure of each token logprob - for token_logprob in content: - assert "token" in token_logprob - assert "logprob" in token_logprob - assert "bytes" in token_logprob - assert "top_logprobs" in token_logprob - - assert isinstance(token_logprob["token"], str) - assert isinstance(token_logprob["logprob"], (int, float)) - assert isinstance(token_logprob["bytes"], list) - assert isinstance(token_logprob["top_logprobs"], list) - - # Validate top_logprobs structure - for top_logprob in token_logprob["top_logprobs"]: - assert "token" in top_logprob - assert "logprob" in top_logprob - assert "bytes" in top_logprob - - def test_chat_completions_logprobs_false( - self, client, model: str, messages: List[dict] - ): - """Test that logprobs=False returns no logprobs.""" - response = client.post( - "/v1/chat/completions", - json={ - "model": model, - "messages": messages, - "logprobs": False, - "max_tokens": 10, - }, - ) - - assert response.status_code == 200 - response_json = response.json() - - # logprobs should be None when logprobs=False - choice = response_json["choices"][0] - assert choice.get("logprobs") is None - - @pytest.mark.parametrize("top_logprobs_value", [0, 5]) - def test_chat_completions_top_logprobs_without_logprobs( - self, - client, - model: str, - messages: List[dict], - top_logprobs_value: int, - backend: str, - ): - """Test that top_logprobs without logprobs raises validation error.""" - if backend != "vllm": - pytest.skip( - reason="logprobs are currently available only for the vLLM backend" - ) - - response = client.post( - "/v1/chat/completions", - json={ - "model": model, - "messages": messages, - "top_logprobs": top_logprobs_value, - "max_tokens": 10, - }, - ) - - # Should raise validation error for any value when logprobs is not True - assert response.status_code == 400 - assert ( - "`top_logprobs` can only be used when `logprobs` is True" - in response.json()["detail"] - ) - - def test_chat_completions_top_logprobs_validation( - self, client, model: str, messages: List[dict] - ): - """Test that top_logprobs > 20 is rejected by schema validation.""" - response = client.post( - "/v1/chat/completions", - json={ - "model": model, - "messages": messages, - "logprobs": True, - "top_logprobs": 25, # Exceeds maximum of 20 - "max_tokens": 5, - }, - ) + @pytest.mark.skip(reason="Not Implemented Yet") + def test_request_logit_bias(self): + pass - # Should raise schema validation error - assert response.status_code == 422 - assert "Input should be less than or equal to 20" in str( - response.json()["detail"] - ) + @pytest.mark.skip(reason="Not Implemented Yet") + def test_usage_response(self): + pass # For tests that won't use the same pytest fixture for server startup across @@ -712,7 +528,7 @@ def test_chat_completions_no_tokenizer( json={"model": model, "messages": messages}, ) - assert response.status_code == 500 + assert response.status_code == 400 assert response.json()["detail"] == "Unknown tokenizer" def test_chat_completions_custom_tokenizer( @@ -726,7 +542,7 @@ def test_chat_completions_custom_tokenizer( # Tokenizers can be provided by a local file path to a directory containing # the relevant files such as tokenizer.json and tokenizer_config.json. custom_tokenizer_path = str(Path(__file__).parent / "custom_tokenizer") - download_cmd = f"hf download --local-dir {custom_tokenizer_path} {tokenizer_model} --include *.json" + download_cmd = f"huggingface-cli download --local-dir {custom_tokenizer_path} {tokenizer_model} --include *.json" print(f"Running download command: {download_cmd}") subprocess.run(download_cmd.split(), check=True) @@ -741,12 +557,7 @@ def test_chat_completions_custom_tokenizer( responses = [] with TestClient(app_local) as client_local, TestClient(app_hf) as client_hf: - payload = { - "model": model, - "messages": messages, - "temperature": 0, - "seed": 0, - } + payload = {"model": model, "messages": messages, "temperature": 0} responses.append(client_local.post("/v1/chat/completions", json=payload)) responses.append(client_hf.post("/v1/chat/completions", json=payload)) @@ -799,7 +610,7 @@ def test_chat_completions_invalid_chat_tokenizer( json={"model": model, "messages": messages}, ) - assert response.status_code == 500 + assert response.status_code == 400 # Error may vary based on transformers version expected_errors = [ "cannot use apply_chat_template()", diff --git a/python/openai/tests/test_completions.py b/python/openai/tests/test_completions.py index 9d496476e1..d89ff4701e 100644 --- a/python/openai/tests/test_completions.py +++ b/python/openai/tests/test_completions.py @@ -1,4 +1,4 @@ -# Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -45,9 +45,8 @@ def test_completions_defaults(self, client, model: str, prompt: str): # NOTE: Could be improved to look for certain quality of response, # or tested with dummy identity model. assert response.json()["choices"][0]["text"].strip() - - usage = response.json().get("usage") - assert usage is not None + # "usage" currently not supported + assert not response.json()["usage"] @pytest.mark.parametrize( "sampling_parameter, value", @@ -57,6 +56,7 @@ def test_completions_defaults(self, client, model: str, prompt: str): ("top_p", 0.9), ("frequency_penalty", 0.5), ("presence_penalty", 0.2), + ("best_of", 1), ("n", 1), # logprobs is an integer for completions ("logprobs", 5), @@ -80,23 +80,10 @@ def test_completions_sampling_parameters( print("Response:", response.json()) # FIXME: Add support and remove this check - unsupported_parameters = ["logit_bias"] + unsupported_parameters = ["logprobs", "logit_bias"] if sampling_parameter in unsupported_parameters: assert response.status_code == 400 - assert response.json()["detail"] == "logit bias is not supported" - return - - # TRT-LLM backend doesn't support logprobs - if ( - sampling_parameter == "logprobs" - and value is not None - and model == "tensorrt_llm_bls" - ): - assert response.status_code == 400 - assert ( - "logprobs are currently available only for the vLLM backend" - in response.json()["detail"] - ) + assert response.json()["detail"] == "logit bias and log probs not supported" return assert response.status_code == 200 @@ -358,12 +345,7 @@ def test_no_prompt(self, client, model: str): ], ) def test_completions_multiple_choices( - self, - client, - sampling_parameter_dict: dict, - backend: str, - model: str, - prompt: str, + self, client, sampling_parameter_dict: dict, model: str, prompt: str ): response = client.post( "/v1/completions", @@ -374,11 +356,7 @@ def test_completions_multiple_choices( # FIXME: Add support and test for success # Expected to fail when n or best_of > 1, only single choice supported for now assert response.status_code == 400 - if backend == "vllm" and "best_of" in sampling_parameter_dict: - error_message = "best_of is no longer supported in vLLM backend" - else: - error_message = "only single choice" - assert error_message in response.json()["detail"] + assert "only single choice" in response.json()["detail"] @pytest.mark.skip(reason="Not Implemented Yet") def test_lora(self): @@ -388,128 +366,6 @@ def test_lora(self): def test_multi_lora(self): pass - @pytest.mark.parametrize("echo", [False, True]) - def test_echo(self, client, model: str, prompt: str, echo: bool): - response = client.post( - "/v1/completions", json={"model": model, "prompt": prompt, "echo": echo} - ) - - response_text = response.json()["choices"][0]["text"].strip() - if echo: - assert response_text.startswith(prompt) - else: - # TODO: Consider using a different prompt. In TRT-LLM model, the second response may contain the prompt in the middle of the response even if echo is False, e.g. " Briefly explained.\nWhat is machine learning? She learns from data\nmachine learning". - assert prompt not in response_text - - def test_usage_response(self, client, model: str, prompt: str): - response = client.post( - "/v1/completions", - json={"model": model, "prompt": prompt}, - ) - - assert response.status_code == 200 - usage = response.json().get("usage") - assert usage is not None - assert isinstance(usage["prompt_tokens"], int) - assert isinstance(usage["completion_tokens"], int) - assert isinstance(usage["total_tokens"], int) - assert usage["prompt_tokens"] > 0 - assert usage["completion_tokens"] > 0 - assert ( - usage["total_tokens"] == usage["prompt_tokens"] + usage["completion_tokens"] - ) - - def test_completions_logprobs(self, client, backend: str, model: str, prompt: str): - """Test logprobs parameter for completions.""" - response = client.post( - "/v1/completions", - json={ - "model": model, - "prompt": prompt, - "logprobs": 3, - "max_tokens": 10, - }, - ) - - # Non-vLLM backends should raise an error - if backend != "vllm": - assert response.status_code == 400 - assert ( - "logprobs are currently available only for the vLLM backend" - in response.json()["detail"] - ) - return - - assert response.status_code == 200 - response_json = response.json() - - # Check that logprobs are present in the response - choice = response_json["choices"][0] - assert "logprobs" in choice - logprobs = choice["logprobs"] - - assert logprobs is not None - assert "text_offset" in logprobs - assert "token_logprobs" in logprobs - assert "tokens" in logprobs - assert "top_logprobs" in logprobs - - assert isinstance(logprobs["text_offset"], list) - assert isinstance(logprobs["token_logprobs"], list) - assert isinstance(logprobs["tokens"], list) - assert isinstance(logprobs["top_logprobs"], list) - - # All lists should have the same length - num_tokens = len(logprobs["tokens"]) - assert len(logprobs["text_offset"]) == num_tokens - assert len(logprobs["token_logprobs"]) == num_tokens - assert len(logprobs["top_logprobs"]) == num_tokens - - # Validate each token - for i in range(num_tokens): - assert isinstance(logprobs["tokens"][i], str) - assert isinstance(logprobs["token_logprobs"][i], (int, float)) - assert isinstance(logprobs["text_offset"][i], int) - assert isinstance(logprobs["top_logprobs"][i], dict) - - # Validate top_logprobs dict contains token -> logprob mappings - for token, logprob in logprobs["top_logprobs"][i].items(): - assert isinstance(token, str) - assert isinstance(logprob, (int, float)) - - def test_completions_logprobs_zero(self, client, model: str, prompt: str): - """Test that logprobs=0 returns no logprobs.""" - response = client.post( - "/v1/completions", - json={ - "model": model, - "prompt": prompt, - "logprobs": 0, - "max_tokens": 10, - }, - ) - - assert response.status_code == 200 - response_json = response.json() - - # logprobs should be None when logprobs=0 - choice = response_json["choices"][0] - assert choice.get("logprobs") is None - - def test_completions_logprobs_validation(self, client, model: str, prompt: str): - """Test that logprobs > 5 is rejected by schema validation.""" - response = client.post( - "/v1/completions", - json={ - "model": model, - "prompt": prompt, - "logprobs": 7, # Exceeds maximum of 5 - "max_tokens": 5, - }, - ) - - # Should raise schema validation error - assert response.status_code == 422 - assert "Input should be less than or equal to 5" in str( - response.json()["detail"] - ) + @pytest.mark.skip(reason="Not Implemented Yet") + def test_usage_response(self): + pass diff --git a/python/openai/tests/test_embeddings.py b/python/openai/tests/test_embeddings.py deleted file mode 100644 index 8462ff14a4..0000000000 --- a/python/openai/tests/test_embeddings.py +++ /dev/null @@ -1,600 +0,0 @@ -# Copyright 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import base64 -import os -from pathlib import Path - -import numpy as np -import pytest - -# Results on A6000 GPU. The results vary slightly across GPU models. -EMBEDDING_OUTPUT_FLOAT = [ - -0.0365707763, - 0.076415509, - 0.0111756483, - 0.0361226462, - -0.0895985439, - 0.000943024294, - 0.0876693726, - -0.0594895333, - 0.0349566936, - -0.0937493294, - 0.132199496, - -0.000293674122, - 0.0232867915, - -0.022317145, - 0.00537066581, - -0.0994819254, - 0.0873948932, - -0.0594052449, - 0.0255441833, - -0.0896684974, - -0.05285028, - 0.0055408217, - -0.0262292251, - -0.0356511623, - 0.00121368305, - 0.0322951674, - 0.0308073629, - 0.0160015952, - -0.0187645368, - -0.00243343855, - -0.031794291, - -0.00363291171, - -0.0096142441, - 0.00763130654, - -0.0644499287, - -0.00359453214, - 0.0715369731, - -0.0835151896, - 0.0429252461, - -0.0022872088, - 0.0263325982, - -0.0283530336, - 0.0369597636, - 0.0226341616, - 0.0176856909, - -0.0551657975, - -0.0439660996, - -0.0230927691, - 0.0370879583, - -0.00998868514, - -0.0479186773, - 0.0299891923, - -0.0363422334, - -0.0903369784, - 0.0643197298, - 0.0223918613, - 0.0467988774, - -0.0922083259, - -0.0229205787, - 0.0192605518, - -0.0187863987, - -0.00113048754, - 0.0447385423, - 0.0520578064, - 0.0455449931, - -0.0587379821, - -0.0332143232, - -0.0847064033, - -0.0296805203, - 0.0522436313, - 0.0216157753, - -0.0455714688, - 0.0491720773, - -0.0230966564, - -0.0275325552, - -0.0460045785, - 0.000419082266, - -0.0763081461, - -0.106194891, - 0.0145287318, - -0.0292449873, - -0.0782747194, - -0.0399451442, - 0.0388121083, - -0.0387705714, - -0.0696005225, - 0.0101006646, - -0.0475178808, - 0.021968415, - 0.0317323506, - 0.0508495905, - -0.0438993014, - -0.0387620702, - -0.0523114018, - 0.0394089296, - -0.0469380654, - -0.0283870418, - -0.023771964, - 0.0526082292, - 0.079620786, - -0.00229133829, - 0.0339719504, - -0.0288904961, - -0.00747278007, - -0.00687310379, - 0.0226157606, - -0.0149615603, - 0.0292317756, - -0.0220453553, - -0.00164654257, - 0.00224191439, - 0.0798983052, - 0.0343615711, - 0.0089583965, - -0.0600032806, - -0.0543279201, - 0.00543989427, - -0.0362421572, - -0.0285114087, - 0.0446144193, - 0.0502739027, - 0.0574940592, - -0.0596095882, - 0.0327727199, - -0.117024891, - -0.0314645469, - 0.136668995, - 0.0, - -0.0752000064, - -0.0291419327, - 0.0460730754, - -0.0266116355, - 0.130121678, - 0.0257908553, - -0.035819497, - 0.0506630391, - -0.0240160581, - 0.0101758447, - 0.0496345982, - -0.0651563033, - -0.0362319537, - 0.00318966783, - 0.0281732827, - 0.0163737591, - -0.0164846163, - 0.0995664597, - 0.0733684897, - 0.00946230628, - -0.00913426094, - -0.0742818192, - 0.06195971, - 0.00591040403, - -0.0686667934, - 0.0317408852, - -0.0137725631, - 0.00940212607, - 0.0279442761, - -0.0201757029, - 0.0324154049, - 0.0228322521, - 0.00267707417, - 0.0171691254, - 0.00419936981, - -0.0130571416, - -0.0173847061, - -0.0758403093, - -0.0392607562, - -0.0166449342, - -0.00168224983, - 0.0199382622, - 0.0526281521, - 0.0502447523, - -0.129478946, - 0.0623795167, - -0.076566115, - 0.0532120988, - 0.0317721888, - -0.0135619631, - -0.0320665911, - -0.0216510575, - 0.106540799, - 0.0604757369, - -0.042855531, - 0.0153838852, - 0.0363844968, - 0.0429414809, - 0.0226413272, - -0.039562691, - 0.0454653203, - 0.0883121043, - 0.0196657199, - -0.0574896857, - 0.00670713792, - -0.018773403, - 0.000347356487, - 0.0141483406, - 0.0345573537, - 0.042631086, - -0.0191322975, - 0.0126261655, - 0.00105785835, - -0.00561600132, - -0.033773981, - 0.0439476408, - -0.0444635749, - -0.035605859, - 0.0268215071, - -0.0402722172, - 0.0911638364, - 0.00135396153, - 0.0485091805, - -0.0246936437, - -0.0408962481, - 0.0829341561, - -0.0306067225, - -0.125902891, - 0.0731693059, - 0.0934899077, - -0.102206372, - 0.0298629422, - 0.0766771212, - -0.0273114517, - -0.024197204, - 0.0, - -0.0244167317, - 0.0970985219, - -0.0935180783, - 0.0111901015, - -0.00198357552, - -0.0769073963, - -0.121803105, - -0.0520681925, - -0.0417099819, - 0.0248719398, - -0.0454342291, - -0.0240311194, - 0.0443824418, - -0.0372257456, - -0.0205930769, - 0.0910829455, - 0.0527612604, - 0.0190431513, - -0.014913708, - 0.0355940796, - -0.00282354676, - 0.0349615514, - 0.020905517, - 0.0863897428, - -0.0470590331, - 0.0979593918, - 0.0291745439, - 0.0513898097, - -0.165631235, - -0.0391068347, - 0.0751543418, - -0.043094065, - 0.028035799, - -0.0311034638, - 0.000913328957, - 0.0944983289, - -0.0198726766, - -0.0259133391, - -0.0108968485, - 0.0387883037, - 0.0520114712, - -0.0322841145, - 0.0131428279, - 0.0847168788, - 0.0164655242, - -0.0242556836, - -0.0100616785, - -0.066205658, - 0.0352719873, - -0.0125291245, - 0.00525686424, - -0.0127795609, - -0.025437668, - -0.0697134733, - -0.0109580038, - 0.00588004105, - 0.0470480993, - -0.0047857468, - 0.0171248112, - -0.0650963187, - -0.0638555363, - -6.33986347e-05, - 0.0477961302, - 0.0663475767, - 0.0779689029, - 0.0126418332, - 0.0279133115, - -0.0708932728, - -0.0341963358, - 0.0108084194, - -0.0322745182, - 0.0595393293, - 0.0120282508, - 0.0222520698, - -0.0312622078, - -0.00225326256, - -0.0878927261, - 0.0264401436, - 0.0213097129, - -0.0696384162, - -0.0348444693, - -0.0397011451, - -0.000856154773, - 0.0166215692, - -0.0223583411, - 0.0652333274, - 0.0340826549, - -0.0526116341, - -0.0165710896, - 0.0189326275, - -0.0277767386, - -0.0210060794, - -0.0209114067, - 0.004393938, - 0.022899719, - -1.13862484e-08, - 0.0633643791, - -0.00489465985, - -0.0535186455, - 0.066366978, - 0.00781527814, - -0.066619873, - 0.0434749424, - -0.008214131, - 0.00729982974, - 0.108025722, - -0.179152384, - 0.0326937772, - 0.0249010883, - 0.0834656358, - 0.0171424076, - 0.0380688161, - 0.0632147491, - -0.0202965494, - -0.0543595888, - 0.053911671, - 0.0329034328, - 0.0403351337, - -0.0204342771, - -0.0667905807, - 0.0286556948, - 0.00270259427, - -0.0699809119, - 0.0458261557, - -0.0122208307, - 0.0477884784, - 0.00767229684, - -0.0723900646, - -0.0811463594, - 0.0289416574, - 0.0698303133, - 0.0109635908, - -0.066716738, - -0.0869814679, - 0.0781401545, - -0.0747744292, - -0.0933830217, - 0.0906731561, - -0.118223637, - -0.00360242673, - 0.00453409506, - 0.0433466882, - -0.0145340748, - 0.101847678, - -0.0519261472, - 0.0441147573, - -0.0348532163, - 0.0241619237, - 0.0494029559, - -0.0146116838, - 0.0442838222, - -0.0998176262, - 0.0255751535, - -0.00209640572, - 0.0171953607, - 0.0489534624, - 0.0367930681, - 0.0853904262, - -0.0312620848, - -0.0702058449, -] - - -@pytest.mark.skipif( - os.environ.get("IMAGE_KIND") == "TRTLLM", - reason="TRT-LLM backend does not support embedding requests", -) -class TestEmbeddings: - @pytest.fixture(scope="class") - def client(self, fastapi_client_class_scope): - yield fastapi_client_class_scope - - @pytest.fixture(scope="class") - def model(self): - # Override with embeddings-specific model - return "all-MiniLM-L6-v2" - - @pytest.fixture(scope="class") - def tokenizer_model(self): - return None - - @pytest.fixture(scope="class") - def model_repository(self): - # Override with embeddings-specific repository - return str(Path(__file__).parent / "vllm_embedding_models") - - @pytest.fixture(scope="class") - def input(self): - return "The food was delicious and the waiter..." - - def _check_embedding_response(self, response, model, encoding_format="float"): - assert response.status_code == 200, response.json() - embedding = response.json()["data"][0]["embedding"] - assert embedding is not None - if encoding_format == "base64": - embedding = np.frombuffer(base64.b64decode(embedding), dtype=np.float32) - - # The results vary slightly across GPU models - result = np.allclose(EMBEDDING_OUTPUT_FLOAT, embedding, rtol=0, atol=1e-3) - assert ( - result - ), f"Embeddings do not match expected output\nExpect {EMBEDDING_OUTPUT_FLOAT},\ngot{embedding}" - - assert response.json()["data"][0]["object"] == "embedding" - assert response.json()["data"][0]["index"] == 0 - assert response.json()["model"] == model - - usage = response.json().get("usage") - assert usage is not None - assert usage["prompt_tokens"] == 12 - assert usage["total_tokens"] == 12 - - @pytest.mark.parametrize( - "input", - [ - "The food was delicious and the waiter...", - [101, 1996, 2833, 2001, 12090, 1998, 1996, 15610, 1012, 1012, 1012, 102], - ], - ) - def test_embeddings_defaults(self, client, model: str, input: str): - response = client.post( - "/v1/embeddings", - json={"model": model, "input": input}, - ) - - self._check_embedding_response(response, model) - - # FIXME: Python model cannot unload gracefully if raise error. - # def test_chat_completions_defaults( - # self, client, model: str, messages: List[dict], backend: str - # ): - # response = client.post( - # "/v1/chat/completions", - # json={"model": model, "messages": messages}, - # ) - - # assert response.status_code == 400 - # assert "does not support" in response.json()["detail"] - - @pytest.mark.parametrize( - "param_key, param_value", - [ - ("encoding_format", "invalid"), - ("encoding_format", 0), - ], - ) - def test_embeddings_invalid_parameters( - self, client, param_key, param_value, model: str, input: str - ): - response = client.post( - "/v1/embeddings", - json={ - "model": model, - "input": input, - param_key: param_value, - }, - ) - - # Assert schema validation error - assert response.status_code == 422, response.json() - - @pytest.mark.parametrize("encoding_format", ["float", "base64"]) - def test_embeddings_parameters( - self, client, encoding_format, model: str, input: str - ): - response = client.post( - "/v1/embeddings", - json={ - "model": model, - "input": input, - "encoding_format": encoding_format, - }, - ) - - self._check_embedding_response(response, model, encoding_format=encoding_format) - - def test_embeddings_empty_request(self, client): - response = client.post("/v1/embeddings", json={}) - assert response.status_code == 422 - assert response.json()["detail"][0]["msg"] == "Field required" - - def test_embeddings_no_model(self, client, input: str): - response = client.post("/v1/embeddings", json={"input": input}) - assert response.status_code == 422 - assert response.json()["detail"][0]["msg"] == "Field required" - - @pytest.mark.parametrize( - "model, error_code", - [ - ("", 400), - (123, 422), - ("Invalid", 400), - (None, 422), - ], - ) - def test_embeddings_invalid_model(self, client, model: str, input, error_code: int): - print("Model:", model) - # Message validation requires min_length of 1 - response = client.post("/v1/embeddings", json={"model": model, "input": input}) - assert response.status_code == error_code - if error_code == 400: - assert response.json()["detail"] == f"Unknown model: {model}" - else: - assert ( - response.json()["detail"][0]["msg"] == "Input should be a valid string" - ) - - def test_embeddings_no_input(self, client, model: str): - response = client.post("/v1/embeddings", json={"model": model}) - assert response.status_code == 422 - - @pytest.mark.parametrize( - "input", - [ - "", - [], - ], - ) - def test_embeddings_empty_input(self, client, model: str, input): - # Message validation requires min_length of 1 - response = client.post("/v1/embeddings", json={"model": model, "input": input}) - assert response.status_code == 422 - assert ( - response.json()["detail"][0]["msg"] - == "Value should have at least 1 item after validation, not 0" - ) - - @pytest.mark.parametrize( - "input", - [ - 123, - 1.5, - 0, - None, - ], - ) - def test_embeddings_invalid_input(self, client, model: str, input): - # Message validation requires min_length of 1 - response = client.post("/v1/embeddings", json={"model": model, "input": input}) - assert response.status_code == 422 - assert response.json()["detail"][0]["msg"] == "Input should be a valid string" diff --git a/python/openai/tests/test_lora.py b/python/openai/tests/test_lora.py deleted file mode 100644 index 0727cbd41f..0000000000 --- a/python/openai/tests/test_lora.py +++ /dev/null @@ -1,497 +0,0 @@ -# Copyright 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import json -import os -import shutil -import unittest - -import pytest -from huggingface_hub import snapshot_download -from openai import BadRequestError, NotFoundError -from openai_frontend.engine.utils.triton import ( - _parse_lora_configs as parse_lora_configs, -) -from openai_frontend.engine.utils.triton import ( - _validate_lora_path_trtllm as validate_lora_path_trtllm, -) - -from .utils import OpenAIServer - - -def is_vllm_installed(): - try: - import vllm as _ - - return True - except ImportError: - return False - - -@pytest.mark.parametrize( - "model_repository,model_name,expect_error", - [ - ("openai_model_repository", "", True), # Empty string as model name. - ("openai_model_repository", " ", True), # Whitespace-only model name. - ("openai_model_repository", "invalid/path", True), - ("openai_model_repository", "invalid\\path", True), - ("openai_model_repository", "../outside/repo", True), - ("openai_model_repository", "../test_models/identity_py", True), - ("test_models", "../test_models/identity_py", True), - ("test_models", "identity_py", False), - ("test_models", "mock_llm", False), - ], -) -def test_parse_lora_configs(model_repository: str, model_name: str, expect_error: bool): - try: - parse_lora_configs(model_repository, model_name, 1, "vllm") - parse_lora_configs(model_repository, model_name, 1, "tensorrtllm") - except ValueError as e: - if expect_error: - assert ( - f"Invalid model name: '{model_name}'. Model names must be valid file-system-path segment names." - == str(e) - ) - else: - raise pytest.fail( - f"(model_repository='{model_repository}', model_name='{model_name}') raised ValueError unexpectedly: {e}" - ) - else: - if expect_error: - raise pytest.fail( - f"(model_repository='{model_repository}', model_name='{model_name}') did not raise ValueError as expected." - ) - - -@pytest.mark.skipif( - is_vllm_installed(), - reason="VLLM backend does not validate LoRA paths", -) -@pytest.mark.parametrize( - "lora_path,expect_error,error_message", - [ - # Valid relative path inside repo (requires .npy files to exist at runtime). - ("tensorrt_llm_bls/1/luotuo-lora-7b-0.1-weights", False, None), - ("tensorrt_llm_bls/1/Japanese-Alpaca-LoRA-7b-v0-weights", False, None), - # Absolute path not allowed. - ( - os.path.join( - os.path.abspath(os.curdir), - "tests/tensorrtllm_models", - "tensorrt_llm_bls/1/luotuo-lora-7b-0.1-weights", - ), - True, - f"must be a relative path inside its model repository", - ), - ("/etc/passwd", True, "must be a relative path inside its model repository"), - # Path outside repo (traversal). - ("tensorrt_llm_bls/1//../1/luotuo-lora-7b-0.1-weights", False, None), - ("../outside/lora", True, "must be inside its model repository"), - ("subdir/../../etc/passwd", True, "must be inside its model repository"), - # LoRA directory not found. - ("tensorrt_llm_bls/10", True, "LoRA directory 'tensorrt_llm_bls/10' not found"), - ( - "tensorrt_llm_bls/1/non_exist", - True, - "LoRA directory 'tensorrt_llm_bls/1/non_exist' not found", - ), - # LoRA file not found. - ("tensorrt_llm_bls/1", True, "LoRA file 'model.lora_weights.npy' not found"), - ], -) -def test_validate_lora_path_trtllm( - lora_path: str, - expect_error: bool, - error_message: str, -): - lora_name = "" - repo_path = "tests/tensorrtllm_models" - try: - validate_lora_path_trtllm(repo_path, lora_path, lora_name) - except Exception as e: - if not expect_error: - raise pytest.fail( - f"repo_path='{repo_path}' raised exception unexpectedly: {e}" - ) - assert error_message in str(e) - else: - if expect_error: - raise pytest.fail( - f"lora_path='{repo_path}' did not raise exception as expected." - ) - - -class LoRATest(unittest.TestCase): - _backend = "vllm" if is_vllm_installed() else "tensorrtllm" - _model_name = "gemma-2b" if _backend == "vllm" else "tensorrt_llm_bls" - # TODO: Find a LoRA model that has its own tokenizer. - _tokenizer = "meta-llama/Meta-Llama-3.1-8B-Instruct" - _lora_separator = "_lora_" - _prompt = "When was the wheel invented?" - # more prompts that may yield different outputs: - # - "Why can camels survive for long without water?" - # - "What is LAPR?" - # - "What is the difference between pets and cattle?" - _temperature = 0 - _top_p = 1 - - def _create_vllm_model_repository_with_lora(self): - shutil.rmtree("models", ignore_errors=True) - os.makedirs(f"models/{self._model_name}/1", exist_ok=True) - with open(f"models/{self._model_name}/config.pbtxt", "w") as f: - f.write('backend: "vllm"') - with open(f"models/{self._model_name}/1/model.json", "w") as f: - f.write( - json.dumps( - { - "model": "unsloth/gemma-2b", - "enable_lora": True, - "max_lora_rank": 32, - } - ) - ) - with open(f"models/{self._model_name}/1/multi_lora.json", "w") as f: - f.write( - json.dumps( - { - "doll": f"models/{self._model_name}/1/GemmaDoll", - "sheep": f"models/{self._model_name}/1/GemmaSheep", - } - ) - ) - snapshot_download( - repo_id="swathijn/GemmaDoll-2b-dolly-LORA-Tune", - local_dir=f"models/{self._model_name}/1/GemmaDoll", - ) - snapshot_download( - repo_id="eduardo-alvarez/GemmaSheep-2B-LORA-TUNED", - local_dir=f"models/{self._model_name}/1/GemmaSheep", - ) - - def _create_trtllm_model_repository_with_lora(self): - shutil.rmtree("models", ignore_errors=True) - shutil.copytree("tests/tensorrtllm_models", "models") - with open(f"models/{self._model_name}/1/multi_lora.json", "w") as f: - f.write( - json.dumps( - { - "doll": f"models/{self._model_name}/1/luotuo-lora-7b-0.1-weights", - "sheep": f"models/{self._model_name}/1/Japanese-Alpaca-LoRA-7b-v0-weights", - } - ) - ) - - def _create_vllm_model_repository_without_lora(self): - shutil.rmtree("models", ignore_errors=True) - os.makedirs(f"models/{self._model_name}/1", exist_ok=True) - with open(f"models/{self._model_name}/config.pbtxt", "w") as f: - f.write('backend: "vllm"') - with open(f"models/{self._model_name}/1/model.json", "w") as f: - f.write(json.dumps({"model": "unsloth/gemma-2b"})) - - def _create_trtllm_model_repository_without_lora(self): - shutil.rmtree("models", ignore_errors=True) - shutil.copytree("tests/tensorrtllm_models", "models") - - def _create_model_repository_mock_llm(self): - shutil.rmtree("models", ignore_errors=True) - os.makedirs(f"models/{self._model_name}/1", exist_ok=True) - with open(f"models/{self._model_name}/config.pbtxt", "w") as f: - f.write( - """ - backend: "python" - max_batch_size: 0 - model_transaction_policy { decoupled: True } - input [ - { - name: "text_input" - data_type: TYPE_STRING - dims: [ 1 ] - }, - { - name: "stream" - data_type: TYPE_BOOL - dims: [ 1 ] - }, - { - name: "sampling_parameters" - data_type: TYPE_STRING - dims: [ 1 ] - }, - { - name: "exclude_input_in_output" - data_type: TYPE_BOOL - dims: [ 1 ] - }, - { - name: "return_num_input_tokens" - data_type: TYPE_BOOL - dims: [1] - optional: true - }, - { - name: "return_num_output_tokens" - data_type: TYPE_BOOL - dims: [1] - optional: true - }, - { - name: "return_logprobs" - data_type: TYPE_BOOL - dims: [1] - optional: true - } - ] - output [ - { - name: "text_output" - data_type: TYPE_STRING - dims: [ -1 ] - } - ] - """ - ) - shutil.copy( - "tests/test_models/mock_llm/1/model.py", f"models/{self._model_name}/1" - ) - - def _get_model_name(self, lora_name): - model_name = self._model_name - if lora_name != "": - model_name += f"{self._lora_separator}{lora_name}" - return model_name - - def _test_list_models(self, client, expected_lora_names): - expected_model_names = [] - for lora_name in expected_lora_names: - expected_model_names.append(self._get_model_name(lora_name)) - models = client.models.list() - for model in models: - if self._backend == "tensorrtllm" and not model.id.startswith( - "tensorrt_llm_bls" - ): - continue - self.assertIn(model.id, expected_model_names) - expected_model_names.remove(model.id) - self.assertEqual( - len(expected_model_names), - 0, - f"expected_model_names: {expected_model_names}", - ) - - def _test_retrieve_model(self, client, lora_name): - model_name = self._get_model_name(lora_name) - model = client.models.retrieve(model_name) - self.assertEqual(model.id, model_name) - - def _test_completions(self, client, lora_name): - model_name = self._get_model_name(lora_name) - completion = client.completions.create( - model=model_name, - prompt=self._prompt, - temperature=self._temperature, - top_p=self._top_p, - ) - self.assertEqual(completion.model, model_name) - self.assertIsNotNone(completion.choices[0].text) - - def _test_chat_completion(self, client, lora_name): - model_name = self._get_model_name(lora_name) - messages = [{"role": "user", "content": self._prompt}] - chat_completion = client.chat.completions.create( - model=model_name, - messages=messages, - temperature=self._temperature, - top_p=self._top_p, - ) - self.assertEqual(chat_completion.model, model_name) - self.assertIsNotNone(chat_completion.choices[0].message.content) - - def test_lora_separator_not_set(self): - if self._backend == "vllm": - self._create_vllm_model_repository_with_lora() - elif self._backend == "tensorrtllm": - self._create_trtllm_model_repository_with_lora() - else: - raise Exception(f"Unexpected backend {self._backend=}") - - with OpenAIServer( - cli_args=[ - "--model-repository", - "models", - "--tokenizer", - self._tokenizer, - ], - env_dict={"CUDA_VISIBLE_DEVICES": "0"}, - ) as server: - client = server.get_client() - # Test listing/retrieving models - self._test_list_models(client, [""]) - self._test_retrieve_model(client, "") - with self.assertRaises(NotFoundError) as e: - self._test_retrieve_model(client, "doll") - expected_error = f"Error code: 404 - {{'detail': 'Unknown model: {self._model_name}{self._lora_separator}doll'}}" - self.assertEqual(str(e.exception), expected_error) - with self.assertRaises(NotFoundError) as e: - self._test_retrieve_model(client, "sheep") - expected_error = f"Error code: 404 - {{'detail': 'Unknown model: {self._model_name}{self._lora_separator}sheep'}}" - self.assertEqual(str(e.exception), expected_error) - # Test selecting LoRAs - self._test_completions(client, "") - self._test_chat_completion(client, "") - with self.assertRaises(BadRequestError) as e: - self._test_completions(client, "doll") - expected_error = f"Error code: 400 - {{'detail': 'Unknown model: {self._model_name}{self._lora_separator}doll'}}" - self.assertEqual(str(e.exception), expected_error) - with self.assertRaises(BadRequestError) as e: - self._test_chat_completion(client, "sheep") - expected_error = f"Error code: 400 - {{'detail': 'Unknown model: {self._model_name}{self._lora_separator}sheep'}}" - self.assertEqual(str(e.exception), expected_error) - - def test_lora_separator_set(self): - if self._backend == "vllm": - self._create_vllm_model_repository_with_lora() - elif self._backend == "tensorrtllm": - self._create_trtllm_model_repository_with_lora() - else: - raise Exception(f"Unexpected backend {self._backend=}") - - with OpenAIServer( - cli_args=[ - "--model-repository", - "models", - "--tokenizer", - self._tokenizer, - "--lora-separator", - self._lora_separator, - ], - env_dict={"CUDA_VISIBLE_DEVICES": "0"}, - ) as server: - client = server.get_client() - # Test listing/retrieving models - self._test_list_models(client, ["", "doll", "sheep"]) - self._test_retrieve_model(client, "") - self._test_retrieve_model(client, "doll") - self._test_retrieve_model(client, "sheep") - - # Test retrieving LoRAs unknown to the backend - with self.assertRaises(NotFoundError) as e: - self._test_retrieve_model(client, "unknown") - expected_error = f"Error code: 404 - {{'detail': 'Unknown model: {self._model_name}{self._lora_separator}unknown'}}" - self.assertEqual(str(e.exception), expected_error) - - # Test selecting LoRAs - self._test_completions(client, "") - self._test_completions(client, "doll") - self._test_completions(client, "sheep") - self._test_chat_completion(client, "") - self._test_chat_completion(client, "doll") - self._test_chat_completion(client, "sheep") - - # Test selecting LoRAs unknown to the backend - expected_error = f"Error code: 400 - {{'detail': 'Unknown LoRA: unknown; for model: {self._model_name}{self._lora_separator}unknown'}}" - with self.assertRaises(BadRequestError) as e: - self._test_completions(client, "unknown") - self.assertEqual(str(e.exception), expected_error) - with self.assertRaises(BadRequestError) as e: - self._test_chat_completion(client, "unknown") - self.assertEqual(str(e.exception), expected_error) - - def test_lora_separator_set_for_lora_off_model(self): - if self._backend == "vllm": - self._create_vllm_model_repository_without_lora() - elif self._backend == "tensorrtllm": - self._create_trtllm_model_repository_without_lora() - else: - raise Exception(f"Unexpected backend {self._backend=}") - - with OpenAIServer( - cli_args=[ - "--model-repository", - "models", - "--tokenizer", - self._tokenizer, - "--lora-separator", - self._lora_separator, - ], - env_dict={"CUDA_VISIBLE_DEVICES": "0"}, - ) as server: - client = server.get_client() - # Test listing/retrieving models - self._test_list_models(client, [""]) - self._test_retrieve_model(client, "") - # Test retrieving models with LoRAs - with self.assertRaises(NotFoundError) as e: - self._test_retrieve_model(client, "doll") - expected_error = f"Error code: 404 - {{'detail': 'Unknown model: {self._model_name}{self._lora_separator}doll'}}" - self.assertEqual(str(e.exception), expected_error) - # Test inference - self._test_completions(client, "") - self._test_chat_completion(client, "") - # Test selecting LoRAs - expected_error = f"Error code: 400 - {{'detail': 'Unknown LoRA: sheep; for model: {self._model_name}{self._lora_separator}sheep'}}" - with self.assertRaises(BadRequestError) as e: - self._test_completions(client, "sheep") - self.assertEqual(str(e.exception), expected_error) - with self.assertRaises(BadRequestError) as e: - self._test_chat_completion(client, "sheep") - self.assertEqual(str(e.exception), expected_error) - - @unittest.skipUnless(is_vllm_installed(), "vLLM not installed") - def test_lora_separator_set_for_non_vllm_formatted_models(self): - self._create_model_repository_mock_llm() - with OpenAIServer( - cli_args=[ - "--model-repository", - "models", - "--tokenizer", - self._tokenizer, - "--backend", - "vllm", - "--lora-separator", - self._lora_separator, - ], - env_dict={"CUDA_VISIBLE_DEVICES": "0"}, - ) as server: - client = server.get_client() - # Test listing/retrieving models - self._test_list_models(client, [""]) - self._test_retrieve_model(client, "") - # Test retrieving models with LoRAs - with self.assertRaises(NotFoundError) as e: - self._test_retrieve_model(client, "sheep") - expected_error = f"Error code: 404 - {{'detail': 'Unknown model: {self._model_name}{self._lora_separator}sheep'}}" - self.assertEqual(str(e.exception), expected_error) - # Test selecting LoRAs - # Expectation: - # If the frontend cannot determine which LoRA(s) are available, then any - # request with a well-formed LoRA model name will be inferenced. - self._test_completions(client, "doll") - self._test_chat_completion(client, "doll") - - -if __name__ == "__main__": - unittest.main() diff --git a/python/openai/tests/test_model_management.py b/python/openai/tests/test_model_management.py deleted file mode 100644 index 7cf420eed1..0000000000 --- a/python/openai/tests/test_model_management.py +++ /dev/null @@ -1,613 +0,0 @@ -# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import concurrent.futures -import http.client -import os -import random -import time -from pathlib import Path -from urllib.parse import urlparse - -import pytest -import requests -from tests.utils import OpenAIServer - -TEST_MODEL_REPOSITORY = str(Path(__file__).parent / "test_models") -TEST_MODEL = "mock_llm" -TEST_MODEL_2 = "identity_py" - - -# Test "--load-model" and "--model-control-mode" CLI options -@pytest.mark.openai -class TestModelControlCLIOptions: - @staticmethod - def _assert_server_launch_fails(args, expected_error: str): - """Helper: verify server fails to start and stderr contains expected_error.""" - with pytest.raises(Exception) as exc_info: - with OpenAIServer(args): - pass - assert expected_error in str(exc_info.value) - - def test_non_explicit_mode_load_model_error(self): - """--load-model without --model-control-mode=explicit must exit with error. - Error message matches native tritonserver exactly.""" - self._assert_server_launch_fails( - args=[ - "--model-repository", - TEST_MODEL_REPOSITORY, - "--load-model", - TEST_MODEL, - ], - expected_error="Error: Use of '--load-model' requires setting '--model-control-mode=explicit' as well.", - ) - - def test_explicit_mode_load_zero_model(self): - args = [ - "--model-repository", - TEST_MODEL_REPOSITORY, - "--model-control-mode", - "explicit", - ] - with OpenAIServer(args) as openai_server: - r = requests.get(openai_server.url_for("v1", "models"), timeout=10) - assert r.status_code == 200 - assert len(r.json()["data"]) == 0 - - def test_explicit_mode_load_one_model(self): - args = [ - "--model-repository", - TEST_MODEL_REPOSITORY, - "--model-control-mode", - "explicit", - "--load-model", - TEST_MODEL, - ] - with OpenAIServer(args) as openai_server: - r = requests.get(openai_server.url_for("v1", "models"), timeout=10) - names = [m["id"] for m in r.json()["data"]] - assert TEST_MODEL in names - assert TEST_MODEL_2 not in names - - def test_explicit_mode_load_multiple_models(self): - args = [ - "--model-repository", - TEST_MODEL_REPOSITORY, - "--model-control-mode", - "explicit", - "--load-model", - TEST_MODEL, - "--load-model", - TEST_MODEL_2, - ] - with OpenAIServer(args) as openai_server: - r = requests.get(openai_server.url_for("v1", "models"), timeout=10) - names = [m["id"] for m in r.json()["data"]] - assert TEST_MODEL in names - assert TEST_MODEL_2 in names - - def test_explicit_mode_load_all_models(self): - args = [ - "--model-repository", - TEST_MODEL_REPOSITORY, - "--model-control-mode", - "explicit", - "--load-model", - "*", - ] - with OpenAIServer(args) as openai_server: - r = requests.get(openai_server.url_for("v1", "models"), timeout=10) - names = [m["id"] for m in r.json()["data"]] - assert TEST_MODEL in names - assert TEST_MODEL_2 in names - - def test_explicit_mode_load_all_models_and_specific_model_error(self): - self._assert_server_launch_fails( - args=[ - "--model-repository", - TEST_MODEL_REPOSITORY, - "--model-control-mode", - "explicit", - "--load-model", - "*", - "--load-model", - TEST_MODEL, - ], - expected_error="Wildcard model name '*' must be the ONLY startup model if specified at all.", - ) - - def test_explicit_mode_load_nonexistent_model_error(self): - self._assert_server_launch_fails( - args=[ - "--model-repository", - TEST_MODEL_REPOSITORY, - "--model-control-mode", - "explicit", - "--load-model", - "nonexistent_model", - ], - expected_error="failed to poll model 'nonexistent_model': model not found in any model repository", - ) - - def test_explicit_mode_load_invalid_model_name_error(self): - invalid_model_names = [ - ( - os.path.relpath("/etc", TEST_MODEL_REPOSITORY), - "at least one version must be available under the version policy", - ), - ( - os.path.relpath("/etc/passwd", TEST_MODEL_REPOSITORY), - "Poll failed for model directory", - ), - ("model/..", "model not found in any model repository"), - ("..", "at least one version must be available under the version policy"), - ("/etc/passwd", "model not found in any model repository"), - ("", "Invalid model name"), - (" ", "model not found in any model repository"), - ("\n\t", "model not found in any model repository"), - ] - for model_name, expected_error in invalid_model_names: - self._assert_server_launch_fails( - args=[ - "--model-repository", - TEST_MODEL_REPOSITORY, - "--model-control-mode", - "explicit", - "--load-model", - model_name, - ], - expected_error=expected_error, - ) - - -class _ModelManagementBase: - @pytest.fixture(scope="class") - def base_url(self, server: OpenAIServer): - return server.url_root - - @pytest.fixture - def all_models(self) -> list[str]: - return [TEST_MODEL, TEST_MODEL_2] - - @pytest.fixture(scope="class") - def load_model(self) -> list[str]: - return [] - - @pytest.fixture(scope="class") - def server( - self, - model_repository: str, - tokenizer_model: str, - backend: str, - model_control_mode: str, - load_model: list[str], - ): - args = [ - "--model-repository", - model_repository, - ] - if tokenizer_model: - args += ["--tokenizer", tokenizer_model] - if backend: - args += ["--backend", backend] - if model_control_mode: - args += ["--model-control-mode", model_control_mode] - if load_model: - for model in load_model: - args += ["--load-model", model] - - with OpenAIServer(args) as openai_server: - yield openai_server - - @pytest.fixture(autouse=True) - def _cleanup(self, base_url, all_models: list[str]): - """Ensure clean state before and after every test by unloading the models.""" - for name in all_models: - requests.post(f"{base_url}/v1/models/{name}/unload") - yield - for name in all_models: - requests.post(f"{base_url}/v1/models/{name}/unload") - - @staticmethod - def _list_available_models(base_url: str) -> list[str]: - response = requests.get(f"{base_url}/v1/models") - assert response.status_code == 200 - return [m["id"] for m in response.json()["data"]] - - @staticmethod - def _assert_unknown_model(response: requests.Response): - assert response.status_code == 400 - assert "unknown model" in response.json()["detail"].lower() - - -class TestModelControlModeNone(_ModelManagementBase): - """Test NONE mode rejects load/unload API calls.""" - - @pytest.fixture(scope="class") - def model_repository(self): - return TEST_MODEL_REPOSITORY - - @pytest.fixture(scope="class") - def model_control_mode(self, request): - return request.param - - @pytest.mark.parametrize("model_control_mode", [None, "none"], indirect=True) - def test_load_and_unload_rejected(self, base_url): - """Test NONE mode rejects load/unload API calls.""" - - for api in ["load", "unload"]: - response = requests.post(f"{base_url}/v1/models/{TEST_MODEL}/{api}") - assert response.status_code == 400 - assert ( - "model load/unload requires --model-control-mode=explicit" - in response.json()["detail"].lower() - ) - - -class TestModelManagement(_ModelManagementBase): - """Test load/unload operations in EXPLICIT mode.""" - - @pytest.fixture(scope="class") - def model_control_mode(self): - return "explicit" - - @pytest.fixture(scope="class") - def model_repository(self): - return TEST_MODEL_REPOSITORY - - @staticmethod - def _assert_model_metadata(model_data: dict): - assert model_data["id"] - assert model_data["object"] == "model" - assert model_data["created"] > 0 - assert model_data["owned_by"] == "Triton Inference Server" - - def test_load_model(self, base_url): - # Server should start with no models loaded - assert self._list_available_models(base_url) == [] - - response = requests.post(f"{base_url}/v1/models/{TEST_MODEL}/load") - assert response.status_code == 200 - self._assert_model_metadata(response.json()) - assert response.json()["id"] == TEST_MODEL - assert TEST_MODEL in self._list_available_models(base_url) - - response = requests.get(f"{base_url}/v1/models/{TEST_MODEL}") - assert response.status_code == 200 - self._assert_model_metadata(response.json()) - - def test_unload_model(self, base_url): - assert ( - requests.post(f"{base_url}/v1/models/{TEST_MODEL}/load").status_code == 200 - ) - - response = requests.post(f"{base_url}/v1/models/{TEST_MODEL}/unload") - assert response.status_code == 200 - body = response.json() - assert body["status"] == "success" - assert body["model"] == TEST_MODEL - - assert TEST_MODEL not in self._list_available_models(base_url) - assert requests.get(f"{base_url}/v1/models/{TEST_MODEL}").status_code == 404 - - def test_load_rejects_duplicate(self, base_url): - assert ( - requests.post(f"{base_url}/v1/models/{TEST_MODEL}/load").status_code == 200 - ) - - response = requests.post(f"{base_url}/v1/models/{TEST_MODEL}/load") - assert response.status_code == 400 - assert "already loaded" in response.json()["detail"].lower() - - def test_load_unload_unknown_model(self, base_url): - response = requests.post(f"{base_url}/v1/models/unknown_model/load") - assert response.status_code == 500 - assert "failed to poll from model repository" in response.json()["detail"] - - response = requests.post(f"{base_url}/v1/models/unknown_model/unload") - self._assert_unknown_model(response) - - def test_load_unload_invalid_model_name(self, base_url): - invalid_model_names = [ - (os.path.relpath("/etc", TEST_MODEL_REPOSITORY), 404), - (os.path.relpath("/etc/passwd", TEST_MODEL_REPOSITORY), 404), - ("model/..", 404), - ("..", 400), - ("/etc/passwd", 404), - ("model/subdir", 404), - ("model/", 404), - ("", 404), - ("%20%20", 400), - ("%0A%09", 400), - ] - parsed = urlparse(base_url) - for model_name, expected_status in invalid_model_names: - for endpoint in ["load", "unload"]: - conn = http.client.HTTPConnection(parsed.hostname, parsed.port) - conn.request("POST", f"/v1/models/{model_name}/{endpoint}") - response = conn.getresponse() - - assert response.status == expected_status, ( - f"Expected {expected_status} for model name {model_name!r}, " - f"got {response.status} {response.read().decode()}" - ) - - def test_load_unload_reload(self, base_url): - assert ( - requests.post(f"{base_url}/v1/models/{TEST_MODEL}/load").status_code == 200 - ) - assert TEST_MODEL in self._list_available_models(base_url) - - assert ( - requests.post(f"{base_url}/v1/models/{TEST_MODEL}/unload").status_code - == 200 - ) - assert TEST_MODEL not in self._list_available_models(base_url) - - assert ( - requests.post(f"{base_url}/v1/models/{TEST_MODEL}/load").status_code == 200 - ) - assert TEST_MODEL in self._list_available_models(base_url) - - def test_load_multiple_models(self, base_url): - assert ( - requests.post(f"{base_url}/v1/models/{TEST_MODEL}/load").status_code == 200 - ) - assert ( - requests.post(f"{base_url}/v1/models/{TEST_MODEL_2}/load").status_code - == 200 - ) - - names = self._list_available_models(base_url) - assert TEST_MODEL in names - assert TEST_MODEL_2 in names - - # Unload the first model - assert ( - requests.post(f"{base_url}/v1/models/{TEST_MODEL}/unload").status_code - == 200 - ) - names = self._list_available_models(base_url) - assert TEST_MODEL not in names - assert TEST_MODEL_2 in names - - # Unload the second model - assert ( - requests.post(f"{base_url}/v1/models/{TEST_MODEL_2}/unload").status_code - == 200 - ) - names = self._list_available_models(base_url) - assert TEST_MODEL not in names - assert TEST_MODEL_2 not in names - - -class TestConcurrentModelManagement(_ModelManagementBase): - """Test sequential and concurrent load/unload.""" - - @pytest.fixture(scope="class") - def model_repository(self): - return TEST_MODEL_REPOSITORY - - @pytest.fixture(scope="class") - def model_control_mode(self): - return "explicit" - - def test_concurrent_load_model(self, base_url): - futures = [] - concurrency = 5 - with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as pool: - for _ in range(concurrency): - futures.append( - pool.submit( - requests.post, f"{base_url}/v1/models/{TEST_MODEL}/load" - ) - ) - - codes = sorted([future.result().status_code for future in futures]) - assert codes == [200] + [400] * ( - concurrency - 1 - ), f"Expected one 200 and the rest 400 for concurrent loads, got {codes}" - assert TEST_MODEL in self._list_available_models(base_url) - - def test_concurrent_unload_model(self, base_url): - # Load the model - requests.post(f"{base_url}/v1/models/{TEST_MODEL}/load") - assert TEST_MODEL in self._list_available_models(base_url) - - futures = [] - concurrency = 5 - with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as pool: - for _ in range(concurrency): - futures.append( - pool.submit( - requests.post, f"{base_url}/v1/models/{TEST_MODEL}/unload" - ) - ) - - codes = sorted([future.result().status_code for future in futures]) - assert codes == [200] + [400] * ( - concurrency - 1 - ), f"Expected one 200 and the rest 400 for concurrent unloads, got {codes}" - assert TEST_MODEL not in self._list_available_models(base_url) - - def test_concurrent_load_multiple_models(self, base_url): - concurrency = 10 - tasks = [] - with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as pool: - for _ in range(concurrency // 2): - for model_name in [TEST_MODEL, TEST_MODEL_2]: - tasks.append( - pool.submit( - requests.post, f"{base_url}/v1/models/{model_name}/load" - ) - ) - - codes = sorted([task.result().status_code for task in tasks]) - assert codes == [200, 200] + [400] * ( - concurrency - 2 - ), f"Expected two 200s and the rest 400 for concurrent loads, got {codes}" - available_models = self._list_available_models(base_url) - assert TEST_MODEL in available_models - assert TEST_MODEL_2 in available_models - - def test_concurrent_unload_multiple_models(self, base_url): - # Load both models - for model_name in [TEST_MODEL, TEST_MODEL_2]: - requests.post(f"{base_url}/v1/models/{model_name}/load") - assert model_name in self._list_available_models(base_url) - - concurrency = 10 - tasks = [] - with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as pool: - for _ in range(concurrency // 2): - for model_name in [TEST_MODEL, TEST_MODEL_2]: - tasks.append( - pool.submit( - requests.post, f"{base_url}/v1/models/{model_name}/unload" - ) - ) - - codes = sorted([task.result().status_code for task in tasks]) - assert codes == [200, 200] + [400] * ( - concurrency - 2 - ), f"Expected two 200s and the rest 400 for concurrent unloads, got {codes}" - available_models = self._list_available_models(base_url) - for model_name in [TEST_MODEL, TEST_MODEL_2]: - assert model_name not in available_models - - def test_concurrent_load_unload_stress(self, base_url): - futures = [] - concurrency = 50 - with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as pool: - for _ in range(concurrency): - action = random.choice(["load", "unload"]) - model_name = random.choice([TEST_MODEL, TEST_MODEL_2]) - futures.append( - pool.submit( - requests.post, f"{base_url}/v1/models/{model_name}/{action}" - ) - ) - done, _ = concurrent.futures.wait( - futures, return_when=concurrent.futures.ALL_COMPLETED - ) - assert ( - len(done) == concurrency - ), f"Expected {concurrency} requests to be completed, got {len(done)}" - for future in done: - response = future.result() - assert response.status_code in ( - 200, - 400, - ), f"Unexpected status code: {response.status_code}" - - # Wait for server to be ready - for _ in range(10): - response = requests.get(f"{base_url}/health/ready") - if response.status_code == 200: - break - time.sleep(1) - assert response.status_code == 200 - - -# Test Inference with real LLM backend (vLLM / TRT-LLM) after load/unload -@pytest.mark.openai -class TestModelManagementInference(_ModelManagementBase): - @pytest.fixture(scope="class") - def model_control_mode(self): - return "explicit" - - @pytest.fixture(scope="class") - def load_model(self, model: str) -> list[str]: - # For tensorrt_llm_bls, we need to load all the dependent models - if model == "tensorrt_llm_bls": - return ["postprocessing", "preprocessing", "tensorrt_llm"] - - @staticmethod - def _assert_load(base_url, model_name: str): - r = requests.post( - f"{base_url}/v1/models/{model_name}/load", - ) - assert r.status_code == 200, f"Load {model_name} failed: {r.text}" - - @staticmethod - def _assert_unload(base_url, model_name: str): - r = requests.post( - f"{base_url}/v1/models/{model_name}/unload", - ) - assert r.status_code == 200, f"Unload {model_name} failed: {r.text}" - - @staticmethod - def _assert_usage(usage): - assert usage is not None - assert usage["prompt_tokens"] > 0 - assert usage["completion_tokens"] > 0 - assert ( - usage["total_tokens"] == usage["prompt_tokens"] + usage["completion_tokens"] - ) - - @staticmethod - def _completions(base_url, model_name: str, **kwargs): - return requests.post( - f"{base_url}/v1/completions", - json={ - "model": model_name, - "prompt": "What is machine learning?", - "max_tokens": 10, - **kwargs, - }, - ) - - def test_load_completions(self, base_url, model: str): - self._assert_unknown_model(self._completions(base_url, model)) - - self._assert_load(base_url, model) - r = self._completions(base_url, model) - assert r.status_code == 200 - data = r.json() - assert data["choices"][0]["text"].strip() - assert data["choices"][0]["finish_reason"] == "stop" - self._assert_usage(data["usage"]) - - self._assert_unload(base_url, model) - - def test_unload_rejects_inference(self, base_url, model: str): - self._assert_load(base_url, model) - assert self._completions(base_url, model).status_code == 200 - - self._assert_unload(base_url, model) - self._assert_unknown_model(self._completions(base_url, model)) - - def test_reload_inference(self, base_url, model: str): - self._assert_load(base_url, model) - assert self._completions(base_url, model).status_code == 200 - - self._assert_unload(base_url, model) - self._assert_unknown_model(self._completions(base_url, model)) - - self._assert_load(base_url, model) - r = self._completions(base_url, model) - assert r.status_code == 200 - assert r.json()["choices"][0]["text"].strip() diff --git a/python/openai/tests/test_models/mock_llm/config.pbtxt b/python/openai/tests/test_models/mock_llm/config.pbtxt index cac10b5de0..5f665ff543 100644 --- a/python/openai/tests/test_models/mock_llm/config.pbtxt +++ b/python/openai/tests/test_models/mock_llm/config.pbtxt @@ -1,4 +1,4 @@ -# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -41,12 +41,6 @@ input [ name: "stream" data_type: TYPE_BOOL dims: [ 1, 1 ] - }, - { - name: "return_logprobs" - data_type: TYPE_BOOL - dims: [ 1, 1 ] - optional: true } ] diff --git a/python/openai/tests/test_openai_client.py b/python/openai/tests/test_openai_client.py index 62fc6bfa18..6f1b456ab4 100644 --- a/python/openai/tests/test_openai_client.py +++ b/python/openai/tests/test_openai_client.py @@ -1,4 +1,4 @@ -# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -26,12 +26,10 @@ from typing import List -import numpy as np import openai import pytest -@pytest.mark.openai class TestOpenAIClient: @pytest.fixture(scope="class") def client(self, server): @@ -61,15 +59,6 @@ def test_openai_client_completion( assert completion.choices[0].text assert completion.choices[0].finish_reason == "stop" - usage = completion.usage - assert usage is not None - assert isinstance(usage.prompt_tokens, int) - assert isinstance(usage.completion_tokens, int) - assert isinstance(usage.total_tokens, int) - assert usage.prompt_tokens > 0 - assert usage.completion_tokens > 0 - assert usage.total_tokens == usage.prompt_tokens + usage.completion_tokens - def test_openai_client_chat_completion( self, client: openai.OpenAI, model: str, messages: List[dict] ): @@ -82,26 +71,22 @@ def test_openai_client_chat_completion( assert chat_completion.choices[0].message.content assert chat_completion.choices[0].finish_reason == "stop" - usage = chat_completion.usage - assert usage is not None - assert isinstance(usage.prompt_tokens, int) - assert isinstance(usage.completion_tokens, int) - assert isinstance(usage.total_tokens, int) - assert usage.prompt_tokens > 0 - assert usage.completion_tokens > 0 - assert usage.total_tokens == usage.prompt_tokens + usage.completion_tokens - @pytest.mark.parametrize("echo", [False, True]) def test_openai_client_completion_echo( - self, client: openai.OpenAI, echo: bool, model: str, prompt: str + self, client: openai.OpenAI, echo: bool, backend: str, model: str, prompt: str ): + if backend == "tensorrtllm": + pytest.skip( + reason="TRT-LLM backend currently only supports setting this parameter at model load time", + ) + completion = client.completions.create(prompt=prompt, model=model, echo=echo) + print(f"Completion results: {completion}") response = completion.choices[0].text if echo: - assert response.startswith(prompt) + assert prompt in response else: - # TODO: Consider using a different prompt. In TRT-LLM model, the second response may contain the prompt in the middle of the response even if echo is False, e.g. " Briefly explained.\nWhat is machine learning? She learns from data\nmachine learning". assert prompt not in response @pytest.mark.skip(reason="Not Implemented Yet") @@ -109,7 +94,6 @@ def test_openai_client_function_calling(self): pass -@pytest.mark.openai class TestAsyncOpenAIClient: @pytest.fixture(scope="class") def client(self, server): @@ -142,15 +126,6 @@ async def test_openai_client_completion( assert completion.choices[0].text assert completion.choices[0].finish_reason == "stop" - usage = completion.usage - assert usage is not None - assert isinstance(usage.prompt_tokens, int) - assert isinstance(usage.completion_tokens, int) - assert isinstance(usage.total_tokens, int) - assert usage.prompt_tokens > 0 - assert usage.completion_tokens > 0 - assert usage.total_tokens == usage.prompt_tokens + usage.completion_tokens - @pytest.mark.asyncio async def test_openai_client_chat_completion( self, client: openai.AsyncOpenAI, model: str, messages: List[dict] @@ -162,16 +137,6 @@ async def test_openai_client_chat_completion( assert chat_completion.choices[0].message.content assert chat_completion.choices[0].finish_reason == "stop" - - usage = chat_completion.usage - assert usage is not None - assert isinstance(usage.prompt_tokens, int) - assert isinstance(usage.completion_tokens, int) - assert isinstance(usage.total_tokens, int) - assert usage.prompt_tokens > 0 - assert usage.completion_tokens > 0 - assert usage.total_tokens == usage.prompt_tokens + usage.completion_tokens - print(f"Chat completion results: {chat_completion}") @pytest.mark.asyncio @@ -185,7 +150,6 @@ async def test_completion_streaming( max_tokens=10, temperature=0.0, stream=False, - seed=0, ) output = chat_completion.choices[0].text stop_reason = chat_completion.choices[0].finish_reason @@ -197,7 +161,6 @@ async def test_completion_streaming( max_tokens=10, temperature=0.0, stream=True, - seed=0, ) chunks = [] finish_reason_count = 0 @@ -235,13 +198,13 @@ async def test_chat_streaming( seed = 0 temperature = 0.0 # Generate enough tokens to easily identify stop words are working. - max_completion_tokens = 64 + max_tokens = 64 # Test single chat completion for comparison chat_completion = await client.chat.completions.create( model=model, messages=messages, - max_completion_tokens=max_completion_tokens, + max_tokens=max_tokens, temperature=temperature, seed=seed, stream=False, @@ -254,7 +217,7 @@ async def test_chat_streaming( stream = await client.chat.completions.create( model=model, messages=messages, - max_completion_tokens=max_completion_tokens, + max_tokens=max_tokens, temperature=temperature, seed=seed, stream=True, @@ -270,7 +233,6 @@ async def test_chat_streaming( chunks.append(delta.content) if chunk.choices[0].finish_reason is not None: finish_reason_count += 1 - assert chunk.usage is None # finish reason should only return in last block assert finish_reason_count == 1 @@ -282,441 +244,7 @@ async def test_chat_streaming( streamed_output = "".join(chunks) assert streamed_output == output + @pytest.mark.skip(reason="Not Implemented Yet") @pytest.mark.asyncio - async def test_chat_streaming_usage_option( - self, client: openai.AsyncOpenAI, model: str, messages: List[dict] - ): - seed = 0 - temperature = 0.0 - max_tokens = 16 - - # Get usage and content from a non-streaming call - stream_false = await client.chat.completions.create( - model=model, - messages=messages, - max_tokens=max_tokens, - temperature=temperature, - seed=seed, - stream=False, - ) - usage_stream_false = stream_false.usage - stream_false_output = stream_false.choices[0].message.content - assert usage_stream_false is not None - assert stream_false_output is not None - - # First, run with include_usage=False. - stream_options_false = await client.chat.completions.create( - model=model, - messages=messages, - max_tokens=max_tokens, - temperature=temperature, - seed=seed, - stream=True, - stream_options={"include_usage": False}, - ) - chunks_false = [chunk async for chunk in stream_options_false] - for chunk in chunks_false: - assert chunk.usage is None, "Usage should be null when include_usage=False" - stream_options_false_output = "".join( - c.choices[0].delta.content - for c in chunks_false - if c.choices and c.choices[0].delta.content - ) - - # Now, run with include_usage=True. - stream_options_true = await client.chat.completions.create( - model=model, - messages=messages, - max_tokens=max_tokens, - temperature=temperature, - seed=seed, - stream=True, - stream_options={"include_usage": True}, - ) - chunks_true = [chunk async for chunk in stream_options_true] - content_chunks = [c for c in chunks_true if c.usage is None] - - # Verify that we received exactly one extra chunk. - assert len(chunks_true) == len(chunks_false) + 1 - - # Verify content is consistent - stream_options_true_output = "".join( - c.choices[0].delta.content - for c in content_chunks - if c.choices and c.choices[0].delta.content - ) - assert stream_options_true_output == stream_false_output - assert stream_options_true_output == stream_options_false_output - - # Verify the final chunk has usage data and empty choices. - final_chunk = chunks_true[-1] - assert final_chunk.usage is not None - assert len(final_chunk.choices) == 0 - usage_stream_options_true = final_chunk.usage - assert ( - isinstance(usage_stream_options_true.prompt_tokens, int) - and usage_stream_options_true.prompt_tokens > 0 - ) - assert ( - isinstance(usage_stream_options_true.completion_tokens, int) - and usage_stream_options_true.completion_tokens > 0 - ) - assert ( - usage_stream_options_true.total_tokens - == usage_stream_options_true.prompt_tokens - + usage_stream_options_true.completion_tokens - ) - - # Verify other chunks have no usage data. - for chunk in chunks_true[:-1]: - assert chunk.usage is None - - # Assert usage is consistent between streaming and non-streaming calls - assert usage_stream_false.model_dump() == usage_stream_options_true.model_dump() - - @pytest.mark.asyncio - async def test_completion_streaming_usage_option( - self, client: openai.AsyncOpenAI, model: str, prompt: str - ): - seed = 0 - temperature = 0.0 - max_tokens = 16 - - # Get usage and content from a non-streaming call - stream_false = await client.completions.create( - model=model, - prompt=prompt, - max_tokens=max_tokens, - temperature=temperature, - stream=False, - seed=seed, - ) - usage_stream_false = stream_false.usage - stream_false_output = stream_false.choices[0].text - assert usage_stream_false is not None - assert stream_false_output is not None - - # First, run with include_usage=False. - stream_options_false = await client.completions.create( - model=model, - prompt=prompt, - max_tokens=max_tokens, - temperature=temperature, - seed=seed, - stream=True, - stream_options={"include_usage": False}, - ) - chunks_false = [chunk async for chunk in stream_options_false] - for chunk in chunks_false: - assert chunk.usage is None - stream_options_false_output = "".join( - c.choices[0].text for c in chunks_false if c.choices and c.choices[0].text - ) - - # Now, run with include_usage=True. - stream_options_true = await client.completions.create( - model=model, - prompt=prompt, - max_tokens=max_tokens, - temperature=temperature, - stream=True, - seed=seed, - stream_options={"include_usage": True}, - ) - chunks_true = [chunk async for chunk in stream_options_true] - content_chunks = [c for c in chunks_true if c.usage is None] - - # Verify that we received exactly one extra chunk. - assert len(chunks_true) == len(chunks_false) + 1 - - # Verify content is consistent - stream_options_true_output = "".join( - c.choices[0].text for c in content_chunks if c.choices and c.choices[0].text - ) - assert stream_options_true_output == stream_false_output - assert stream_options_true_output == stream_options_false_output - - # Verify the final chunk has usage data and empty choices. - final_chunk = chunks_true[-1] - assert final_chunk.usage is not None - assert len(final_chunk.choices) == 0 - usage_stream_options_true = final_chunk.usage - assert ( - isinstance(usage_stream_options_true.prompt_tokens, int) - and usage_stream_options_true.prompt_tokens > 0 - ) - assert ( - isinstance(usage_stream_options_true.completion_tokens, int) - and usage_stream_options_true.completion_tokens > 0 - ) - assert ( - usage_stream_options_true.total_tokens - == usage_stream_options_true.prompt_tokens - + usage_stream_options_true.completion_tokens - ) - - # Verify other chunks have no usage data. - for chunk in chunks_true[:-1]: - assert chunk.usage is None - - # Assert usage is consistent between streaming and non-streaming calls - assert usage_stream_false.model_dump() == usage_stream_options_true.model_dump() - - @pytest.mark.asyncio - async def test_stream_options_without_streaming( - self, client: openai.AsyncOpenAI, model: str, prompt: str - ): - with pytest.raises(openai.BadRequestError) as e: - await client.completions.create( - model=model, - prompt=prompt, - stream=False, - stream_options={"include_usage": True}, - ) - assert "`stream_options` can only be used when `stream` is True" in str(e.value) - - with pytest.raises(openai.BadRequestError) as e: - await client.chat.completions.create( - model=model, - messages=[{"role": "user", "content": prompt}], - stream=False, - stream_options={"include_usage": True}, - ) - assert "`stream_options` can only be used when `stream` is True" in str(e.value) - - @pytest.mark.asyncio - async def test_chat_completion_logprobs( - self, client: openai.AsyncOpenAI, backend: str, model: str, messages: List[dict] - ): - """Test logprobs for chat completions and compare streaming vs non-streaming.""" - # Non-vLLM backends should raise an error - if backend != "vllm": - with pytest.raises(openai.BadRequestError) as exc_info: - await client.chat.completions.create( - model=model, - messages=messages, - logprobs=True, - top_logprobs=2, - max_tokens=10, - ) - assert "logprobs are currently available only for the vLLM backend" in str( - exc_info.value - ) - return - - # Test non-streaming - seed = 0 - temperature = 0.0 - chat_completion = await client.chat.completions.create( - model=model, - messages=messages, - logprobs=True, - top_logprobs=2, - max_tokens=10, - temperature=temperature, - seed=seed, - stream=False, - ) - - assert chat_completion.choices[0].message.content - assert chat_completion.choices[0].logprobs is not None - - logprobs = chat_completion.choices[0].logprobs - assert logprobs.content is not None - assert len(logprobs.content) > 0 - - # Validate each token logprob - for token_logprob in logprobs.content: - assert token_logprob.token - assert isinstance(token_logprob.logprob, float) - assert isinstance(token_logprob.bytes, list) - assert token_logprob.top_logprobs is not None - assert len(token_logprob.top_logprobs) > 0 - - # Test streaming and compare with non-streaming - stream = await client.chat.completions.create( - model=model, - messages=messages, - logprobs=True, - top_logprobs=2, - max_tokens=10, - temperature=temperature, - seed=seed, - stream=True, - ) - - chunks = [] - stream_logprobs = [] - async for chunk in stream: - if chunk.choices[0].delta.content: - chunks.append(chunk.choices[0].delta.content) - if chunk.choices[0].logprobs and chunk.choices[0].logprobs.content: - stream_logprobs.extend(chunk.choices[0].logprobs.content) - - # Assert streaming output matches non-streaming - streamed_output = "".join(chunks) - assert streamed_output == chat_completion.choices[0].message.content - - # Assert both streaming and non-streaming produce logprobs - assert len(stream_logprobs) > 0, "Streaming should produce logprobs" - assert len(stream_logprobs) == len(logprobs.content), "Same number of tokens" - - # Compare tokens and logprob values (using np.allclose for float comparison) - stream_tokens_list = [t.token for t in stream_logprobs] - non_stream_tokens_list = [t.token for t in logprobs.content] - stream_logprobs_values = [t.logprob for t in stream_logprobs] - non_stream_logprobs_values = [t.logprob for t in logprobs.content] - - assert stream_tokens_list == non_stream_tokens_list, "Tokens should match" - assert np.allclose( - stream_logprobs_values, non_stream_logprobs_values, rtol=0, atol=1e-1 - ), "Logprob values should be close" - - @pytest.mark.asyncio - async def test_completion_logprobs( - self, client: openai.AsyncOpenAI, backend: str, model: str, prompt: str - ): - """Test logprobs for completions.""" - # Non-vLLM backends should raise an error - if backend != "vllm": - with pytest.raises(openai.BadRequestError) as exc_info: - await client.completions.create( - model=model, - prompt=prompt, - logprobs=3, - max_tokens=10, - ) - assert "logprobs are currently available only for the vLLM backend" in str( - exc_info.value - ) - return - - # Test non-streaming - seed = 0 - temperature = 0.0 - completion = await client.completions.create( - model=model, - prompt=prompt, - logprobs=3, - max_tokens=10, - temperature=temperature, - seed=seed, - stream=False, - ) - - assert completion.choices[0].text - assert completion.choices[0].logprobs is not None - - logprobs = completion.choices[0].logprobs - assert logprobs.tokens is not None - assert logprobs.token_logprobs is not None - assert logprobs.text_offset is not None - assert logprobs.top_logprobs is not None - - num_tokens = len(logprobs.tokens) - assert len(logprobs.token_logprobs) == num_tokens - assert len(logprobs.text_offset) == num_tokens - assert len(logprobs.top_logprobs) == num_tokens - - # Test streaming and compare with non-streaming - stream = await client.completions.create( - model=model, - prompt=prompt, - logprobs=3, - max_tokens=10, - temperature=temperature, - seed=seed, - stream=True, - ) - - chunks = [] - stream_tokens = [] - stream_token_logprobs = [] - stream_text_offsets = [] - stream_top_logprobs = [] - - async for chunk in stream: - if chunk.choices[0].text: - chunks.append(chunk.choices[0].text) - if chunk.choices[0].logprobs: - lp = chunk.choices[0].logprobs - if lp.tokens: - stream_tokens.extend(lp.tokens) - if lp.token_logprobs: - stream_token_logprobs.extend(lp.token_logprobs) - if lp.text_offset: - stream_text_offsets.extend(lp.text_offset) - if lp.top_logprobs: - stream_top_logprobs.extend(lp.top_logprobs) - - # Assert streaming output matches non-streaming - streamed_output = "".join(chunks) - assert streamed_output == completion.choices[0].text - - # Compare values (using np.allclose for float comparison) - assert stream_tokens == logprobs.tokens, "Tokens should match" - assert stream_text_offsets == logprobs.text_offset, "Text offsets should match" - assert stream_top_logprobs == logprobs.top_logprobs, "Top logprobs should match" - assert np.allclose( - stream_token_logprobs, logprobs.token_logprobs, rtol=0, atol=1e-1 - ), "Token logprob values should be close" - - @pytest.mark.parametrize("top_logprobs_value", [0, 5]) - @pytest.mark.asyncio - async def test_top_logprobs_requires_logprobs( - self, - client: openai.AsyncOpenAI, - model: str, - messages: List[dict], - top_logprobs_value: int, - backend: str, - ): - """ - Test that top_logprobs without logprobs raises an error - """ - if backend != "vllm": - pytest.skip( - reason="logprobs are currently available only for the vLLM backend" - ) - - with pytest.raises(openai.BadRequestError) as exc_info: - await client.chat.completions.create( - model=model, - messages=messages, - top_logprobs=top_logprobs_value, # Without logprobs=True - max_tokens=5, - ) - assert "`top_logprobs` can only be used when `logprobs` is True" in str( - exc_info.value - ) - - @pytest.mark.asyncio - async def test_chat_top_logprobs_exceeds_max( - self, client: openai.AsyncOpenAI, model: str, messages: List[dict] - ): - """Test that top_logprobs > 20 raises schema validation error.""" - with pytest.raises(openai.UnprocessableEntityError) as exc_info: - await client.chat.completions.create( - model=model, - messages=messages, - logprobs=True, - top_logprobs=25, # Exceeds maximum of 20 - max_tokens=5, - ) - # Pydantic validation error - assert "less than or equal to 20" in str(exc_info.value).lower() - - @pytest.mark.asyncio - async def test_completion_logprobs_exceeds_max( - self, client: openai.AsyncOpenAI, model: str, prompt: str - ): - """Test that logprobs > 5 raises schema validation error.""" - with pytest.raises(openai.UnprocessableEntityError) as exc_info: - await client.completions.create( - model=model, - prompt=prompt, - logprobs=7, # Exceeds maximum of 5 - max_tokens=5, - ) - # Pydantic validation error - assert "less than or equal to 5" in str(exc_info.value).lower() + async def test_openai_client_function_calling(self): + pass diff --git a/python/openai/tests/test_openai_restricted_apis.py b/python/openai/tests/test_openai_restricted_apis.py deleted file mode 100755 index 200412df34..0000000000 --- a/python/openai/tests/test_openai_restricted_apis.py +++ /dev/null @@ -1,467 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -from pathlib import Path -from typing import Dict, List, Optional - -import pytest -import requests -from tests.utils import OpenAIServer - - -def assert_response_success( - response: requests.Response, expected_status: int = 200, description: str = "" -): - """Assert that a response was successful.""" - assert ( - response.status_code == expected_status - ), f"{description} should return {expected_status}, got {response.status_code} {response.text}" - - -def assert_response_unauthorized( - response: requests.Response, expected_status: int = 401, description: str = "" -): - """Assert that a response was unauthorized.""" - assert ( - response.status_code == expected_status - ), f"{description} should be unauthorized with {expected_status}, got {response.status_code} {response.text}" - - -def make_get_request( - base_url: str, - endpoint: str, - headers: Optional[Dict[str, str]] = None, - timeout: int = 10, -): - """Make a GET request to the specified endpoint.""" - url = f"{base_url}{endpoint}" - response = requests.get(url, headers=headers, timeout=timeout) - return response - - -def verify_inference_endpoints( - base_url, model, headers, expected_success, description_prefix -): - def make_chat_request( - base_url: str, - model: str, - messages: List[Dict[str, str]], - headers: Optional[Dict[str, str]] = None, - max_tokens: int = 10, - timeout: int = 10, - ): - """Make a POST request to the chat completions endpoint.""" - url = f"{base_url}/v1/chat/completions" - data = { - "model": model, - "messages": messages, - "max_tokens": max_tokens, - } - response = requests.post(url, json=data, headers=headers, timeout=timeout) - return response - - def make_completion_request( - base_url: str, - model: str, - prompt: str, - headers: Optional[Dict[str, str]] = None, - max_tokens: int = 10, - timeout: int = 10, - ): - """Make a POST request to the completions endpoint.""" - url = f"{base_url}/v1/completions" - data = { - "model": model, - "prompt": prompt, - "max_tokens": max_tokens, - } - response = requests.post(url, json=data, headers=headers, timeout=timeout) - return response - - messages = [{"role": "user", "content": "Hello"}] - response = make_chat_request(base_url, model, messages, headers=headers) - if expected_success: - assert_response_success( - response, description=f"{description_prefix} Chat completions endpoint" - ) - else: - assert_response_unauthorized( - response, description=f"{description_prefix} Chat completions endpoint" - ) - - prompt = "Hello" - response = make_completion_request(base_url, model, prompt, headers=headers) - if expected_success: - assert_response_success( - response, description=f"{description_prefix} Completions endpoint" - ) - else: - assert_response_unauthorized( - response, description=f"{description_prefix} Completions endpoint" - ) - - -def verify_model_repository_endpoints( - base_url, model, headers, expected_success, description_prefix -): - # Verify model repository endpoints - response = make_get_request(base_url, "/v1/models", headers=headers) - if expected_success: - assert_response_success( - response, description=f"{description_prefix} Models endpoint" - ) - else: - assert_response_unauthorized( - response, description=f"{description_prefix} Models endpoint" - ) - - response = make_get_request(base_url, f"/v1/models/{model}", headers=headers) - if expected_success: - assert_response_success( - response, description=f"{description_prefix} Specific model endpoint" - ) - else: - assert_response_unauthorized( - response, description=f"{description_prefix} Specific model endpoint" - ) - - # Verify model management endpoints - for endpoint in ["unload", "load"]: - response = requests.post( - f"{base_url}/v1/models/{model}/{endpoint}", headers=headers - ) - if expected_success: - assert_response_success( - response, - description=f"{description_prefix} - Model {endpoint} endpoint", - ) - else: - assert_response_unauthorized( - response, - description=f"{description_prefix} - Model {endpoint} endpoint", - ) - - -def verify_metrics_endpoint(base_url, headers, expected_success, description_prefix): - # Test metrics endpoint - response = make_get_request(base_url, "/metrics", headers=headers) - assert_response_success(response, description="Unrestricted Metrics endpoint") - - if expected_success: - assert_response_success( - response, description=f"{description_prefix} Metrics endpoint" - ) - else: - assert_response_unauthorized( - response, description=f"{description_prefix} Metrics endpoint" - ) - - -def verify_health_endpoint(base_url, headers, expected_success, description_prefix): - # Test health endpoint - response = make_get_request(base_url, "/health/ready", headers=headers) - if expected_success: - assert_response_success( - response, description=f"{description_prefix} Health endpoint" - ) - else: - assert_response_unauthorized( - response, description=f"{description_prefix} Health endpoint" - ) - - -@pytest.mark.openai -class TestRestrictedAPIInvalidArguments: - """Test cases for malformed --openai-restricted-api arguments.""" - - def _test_server_startup_failure( - self, - malformed_api_arg, - expected_error_pattern=None, - ): - """Helper method to test that server fails to start with malformed arguments.""" - args = [ - "--model-repository", - str( - Path(__file__).parent / f"test_models" - ), # Hardcode to simple models to speed up tests - ] - if type(malformed_api_arg[0]) == list: - for api_arg in malformed_api_arg: - args.append("--openai-restricted-api") - args.extend(api_arg) - else: - args.append("--openai-restricted-api") - args.extend(malformed_api_arg) - - # Server should fail to start with malformed arguments - with pytest.raises((ValueError, Exception)) as exc_info: - with OpenAIServer(args) as openai_server: - pass # Should not reach here - - if expected_error_pattern: - assert expected_error_pattern in str( - exc_info.value - ), f"Expected error pattern '{expected_error_pattern}' not found in: {exc_info.value}" - - @pytest.mark.parametrize( - "malformed_arg", - [ - ["unknown-endpoint", "auth-key", "auth-value"], - ["invalid,inference", "auth-key", "auth-value"], # Mix of invalid and valid - ["inference,unknown", "auth-key", "auth-value"], # Mix of valid and invalid - ], - ) - def test_unknown_endpoint_names(self, malformed_arg): - """Test that server handles unknown endpoint names gracefully.""" - self._test_server_startup_failure( - malformed_arg, - expected_error_pattern="Unknown API", - ) - - @pytest.mark.parametrize( - "malformed_arg", - [ - ["inference,inference", "auth-key", "auth-value"], - ], - ) - def test_duplicate_apis(self, malformed_arg): - """Test that server handles duplicate APIs gracefully.""" - self._test_server_startup_failure( - malformed_arg, - expected_error_pattern="restricted api 'inference' can not be specified in multiple config groups", - ) - - @pytest.mark.parametrize( - "malformed_arg", - [ - # API with different auth specs - [ - ["inference", "auth-key1", "value1"], - ["inference", "auth-key2", "value2"], - ], - # API with same auth specs - [["inference", "auth-key", "value"], ["inference", "auth-key", "value"]], - # Multiple APIs with one duplicate - [ - ["inference", "auth-key1", "value1"], - ["model-repository", "auth-key2", "value2"], - ["inference", "auth-key3", "value3"], - ], - # All APIs duplicated - [ - ["inference", "auth-key1", "value1"], - ["model-repository", "auth-key2", "value2"], - ["inference", "auth-key3", "value3"], - ["model-repository", "auth-key4", "value4"], - ], - ], - ) - def test_conflict_configs(self, malformed_arg): - """Test that server fails when duplicate APIs are specified in multiple arguments.""" - # Test cases where the same API name appears in multiple --openai-restricted-api arguments - self._test_server_startup_failure( - malformed_arg, - expected_error_pattern="restricted api 'inference' can not be specified in multiple config groups", - ) - - -@pytest.mark.openai -class TestOpenAIServerRestrictedAPIs: - """Test cases for OpenAI server with restricted APIs functionality.""" - - @pytest.fixture(scope="class") - def server_with_restrictions(self, model_repository, tokenizer_model, backend): - """Start server with restricted APIs enabled.""" - args = [ - "--model-repository", - model_repository, - "--tokenizer", - tokenizer_model, - "--backend", - backend, - "--model-control-mode", - "explicit", - "--load-model", - "*", - "--openai-restricted-api", - "inference,model-repository", - "admin-key", - "admin-value", - ] - - with OpenAIServer(args) as openai_server: - yield openai_server - - @pytest.mark.parametrize( - "headers, expected_success, description", - [ - (None, False, "No auth"), - ({"admin-key": "admin-value"}, True, "Valid auth"), - ({"admin-key": "wrong-value"}, False, "Invalid auth value"), - ({"wrong-key": "admin-value"}, False, "Invalid auth key"), - ], - ) - def test_restricted_endpoints_with_auth( - self, server_with_restrictions, model, headers, expected_success, description - ): - """Test restricted endpoints with different authentication scenarios.""" - base_url = server_with_restrictions.url_root - - verify_model_repository_endpoints( - base_url, model, headers, expected_success, description - ) - verify_inference_endpoints( - base_url, model, headers, expected_success, description - ) - - def test_unrestricted_endpoints(self, server_with_restrictions): - """Test that unrestricted endpoints work without authentication.""" - base_url = server_with_restrictions.url_root - - verify_metrics_endpoint( - base_url, None, expected_success=True, description_prefix="Unrestricted" - ) - verify_health_endpoint( - base_url, None, expected_success=True, description_prefix="Unrestricted" - ) - - -@pytest.mark.openai -class TestOpenAIServerMultipleRestrictions: - """Test cases for OpenAI server with multiple restriction groups.""" - - @pytest.fixture(scope="class") - def server_multiple_restrictions(self, model_repository, tokenizer_model, backend): - """Start server with multiple restriction groups.""" - args = [ - "--model-repository", - model_repository, - "--tokenizer", - tokenizer_model, - "--backend", - backend, - "--model-control-mode", - "explicit", - "--load-model", - "*", - "--openai-restricted-api", - "model-repository", - "model-key", - "model-value", - "--openai-restricted-api", - "inference", - "infer-key", - "infer-value", - ] - - with OpenAIServer(args) as openai_server: - yield openai_server - - def test_endpoint_groups_with_correct_auth( - self, server_multiple_restrictions, model - ): - """Test that endpoint groups work with their specific authentication keys.""" - base_url = server_multiple_restrictions.url_root - - # Test model repository endpoints with model key - model_headers = {"model-key": "model-value"} - verify_model_repository_endpoints( - base_url, - model, - model_headers, - expected_success=True, - description_prefix="Correct model key", - ) - - # Test inference endpoints with inference key - infer_headers = {"infer-key": "infer-value"} - verify_inference_endpoints( - base_url, - model, - infer_headers, - expected_success=True, - description_prefix="Correct inference key", - ) - - @pytest.mark.parametrize( - "model_headers, model_description, infer_headers, infer_description", - [ - (None, "No auth", None, "No auth"), - ( - {"infer-key": "infer-value"}, - "Model key for inference endpoints", - {"model-key": "model-value"}, - "Inference key for model endpoints", - ), - ( - {"wrong-key": "wrong-value"}, - "Completely wrong key", - {"wrong-key": "wrong-value"}, - "Completely wrong key", - ), - ], - ) - def test_endpoint_groups_with_wrong_auth( - self, - server_multiple_restrictions, - model, - model_headers, - model_description, - infer_headers, - infer_description, - ): - """Test that endpoint groups are blocked with wrong authentication keys.""" - base_url = server_multiple_restrictions.url_root - - # Test scenarios where wrong auth keys are used - verify_model_repository_endpoints( - base_url, - model, - model_headers, - expected_success=False, - description_prefix=model_description, - ) - verify_inference_endpoints( - base_url, - model, - infer_headers, - expected_success=False, - description_prefix=infer_description, - ) - - def test_unrestricted_endpoints(self, server_multiple_restrictions): - """Test that unrestricted endpoints work without authentication.""" - base_url = server_multiple_restrictions.url_root - - verify_metrics_endpoint( - base_url, None, expected_success=True, description_prefix="Unrestricted" - ) - verify_health_endpoint( - base_url, None, expected_success=True, description_prefix="Unrestricted" - ) diff --git a/python/openai/tests/test_request_size.py b/python/openai/tests/test_request_size.py deleted file mode 100644 index 5670d00a7a..0000000000 --- a/python/openai/tests/test_request_size.py +++ /dev/null @@ -1,165 +0,0 @@ -# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -import asyncio -import json -import os -import sys -from pathlib import Path - -import pytest -import tritonserver -from fastapi.testclient import TestClient - -sys.path.append( - os.path.join(str(Path(__file__).resolve().parent.parent), "openai_frontend") -) - -from frontend.fastapi.middleware.request_size import RequestSizeLimitMiddleware -from tests.utils import setup_fastapi_app, setup_server -from utils.utils import HTTP_DEFAULT_MAX_INPUT_SIZE - -_MODEL = "mock_llm" - -# All POST endpoints. -_ENDPOINTS = ( - "/v1/chat/completions", - "/v1/completions", - "/v1/embeddings", - f"/v1/models/{_MODEL}/load", - f"/v1/models/{_MODEL}/unload", -) - - -@pytest.fixture(scope="module") -def client(): - """FastApiFrontend backed by a real Triton server with mock_llm loaded.""" - model_repository = str(Path(__file__).parent / "test_models") - server = setup_server( - model_repository, - model_control_mode=tritonserver.ModelControlMode.EXPLICIT, - load_models=[_MODEL], - ) - try: - app = setup_fastapi_app(tokenizer="", server=server, backend=None) - with TestClient(app) as test_client: - yield test_client - finally: - server.stop() - - -def _assert_content_too_large(response, actual_bytes: int) -> None: - assert response.status_code == 413 - body = response.json() - assert set(body) == {"error"} - error = body["error"] - assert error["type"] == "invalid_request_error" - assert error["code"] == "content_too_large" - assert error["message"] == RequestSizeLimitMiddleware._oversized_request_message( - actual_bytes, HTTP_DEFAULT_MAX_INPUT_SIZE - ) - - -class TestRequestSizeLimitMiddleware: - @pytest.mark.parametrize("endpoint", _ENDPOINTS) - def test_body_at_limit_is_not_rejected(self, client, endpoint): - response = client.post(endpoint, content=b"x" * HTTP_DEFAULT_MAX_INPUT_SIZE) - assert response.status_code != 413 - - @pytest.mark.parametrize("endpoint", _ENDPOINTS) - def test_body_over_limit_is_rejected(self, client, endpoint): - over = HTTP_DEFAULT_MAX_INPUT_SIZE + 1 - response = client.post(endpoint, content=b"x" * over) - _assert_content_too_large(response, over) - - @pytest.mark.parametrize("endpoint", _ENDPOINTS) - def test_chunked_body_over_limit_is_rejected(self, client, endpoint): - over = HTTP_DEFAULT_MAX_INPUT_SIZE + 1 - - # httpx switches to chunked transfer when content is an Iterable[bytes]. - def chunks(): - yield b"x" * HTTP_DEFAULT_MAX_INPUT_SIZE - yield b"x" - - response = client.post(endpoint, content=chunks()) - _assert_content_too_large(response, over) - - def test_get_without_body_is_unaffected(self, client): - response = client.get(_ENDPOINTS[0]) - assert response.status_code == 405 - - -class TestContentLengthValidation: - """Stage 1 rejects malformed Content-Length with 400.""" - - def _run_with_content_length(self, raw_value: bytes) -> tuple[int, dict]: - captured: dict = {"status": None, "body": b""} - - async def app(scope, receive, send): - raise AssertionError("app must not be reached for invalid Content-Length") - - async def receive(): - raise AssertionError("receive() must not be called when Stage 1 rejects") - - async def send(message): - if message["type"] == "http.response.start": - captured["status"] = message["status"] - elif message["type"] == "http.response.body": - captured["body"] += message.get("body", b"") - - middleware = RequestSizeLimitMiddleware( - app=app, http_max_input_size=HTTP_DEFAULT_MAX_INPUT_SIZE - ) - scope = { - "type": "http", - "method": "POST", - "path": "/v1/chat/completions", - "headers": [(b"content-length", raw_value)], - } - asyncio.run(middleware(scope, receive, send)) - - assert captured["status"] is not None, "no response was sent" - return captured["status"], json.loads(captured["body"]) - - def _assert_invalid_content_length(self, status: int, body: dict): - assert status == 400 - assert set(body) == {"error"} - error = body["error"] - assert error["type"] == "invalid_request_error" - assert error["code"] == "invalid_content_length" - - @pytest.mark.parametrize("raw", [b"not-a-number", b""]) - def test_non_integer_content_length_rejected_with_400(self, raw): - status, body = self._run_with_content_length(raw) - self._assert_invalid_content_length(status, body) - assert "not an integer" in body["error"]["message"] - - @pytest.mark.parametrize("raw", [b"-1", b"-1024"]) - def test_negative_content_length_rejected_with_400(self, raw): - status, body = self._run_with_content_length(raw) - self._assert_invalid_content_length(status, body) - assert "non-negative" in body["error"]["message"] diff --git a/python/openai/tests/test_tool_calling.py b/python/openai/tests/test_tool_calling.py deleted file mode 100644 index fccb27ac03..0000000000 --- a/python/openai/tests/test_tool_calling.py +++ /dev/null @@ -1,594 +0,0 @@ -# Copyright (c) 2025-2026, NVIDIA CORPORATION. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import json -import os -from typing import Dict, List, Optional - -import openai -import pytest -from openai.types.chat import ( - ChatCompletionMessageParam, - ChatCompletionMessageToolCall, - ChatCompletionNamedToolChoiceParam, - ChatCompletionToolParam, -) - -# resources for testing the tool callings -WEATHER_TOOL: ChatCompletionToolParam = { - "type": "function", - "function": { - "name": "get_current_weather", - "description": "Get the current weather in a given location", - "parameters": { - "type": "object", - "properties": { - "city": { - "type": "string", - "description": "The city to find the weather for, " - "e.g. 'San Francisco'", - }, - "state": { - "type": "string", - "description": "must the two-letter abbreviation for the state " - "that the city is in, e.g. 'CA' which would " - "mean 'California'", - }, - "unit": { - "type": "string", - "description": "The unit to fetch the temperature in", - "enum": ["celsius", "fahrenheit"], - }, - }, - "required": ["city", "state", "unit"], - }, - }, -} - -WEATHER_FORECAST_TOOL: ChatCompletionToolParam = { - "type": "function", - "function": { - "name": "get_n_day_weather_forecast", - "description": "Get an N-day weather forecast", - "parameters": { - "type": "object", - "properties": { - "city": { - "type": "string", - "description": "The city to find the weather for, " - "e.g. 'San Francisco'", - }, - "state": { - "type": "string", - "description": "must the two-letter abbreviation for the state " - "that the city is in, e.g. 'CA' which would " - "mean 'California'", - }, - "unit": { - "type": "string", - "description": "The unit to fetch the temperature in", - "enum": ["celsius", "fahrenheit"], - }, - "num_days": { - "type": "integer", - "description": "The number of days to forecast", - }, - }, - "required": ["city", "state", "unit", "num_days"], - }, - }, -} - -MESSAGES_ASKING_FOR_TOOLS: List[ChatCompletionMessageParam] = [ - { - "role": "system", - "content": "You're a helpful assistant! Answer the users question best you can.", - }, - {"role": "user", "content": "What is the weather in Dallas, Texas in Fahrenheit?"}, -] - -MESSAGES_WITH_TOOL_RESPONSE: List[ChatCompletionMessageParam] = [ - { - "role": "system", - "content": "You're a helpful assistant! Answer the users question best you can.", - }, - {"role": "user", "content": "What is the weather in Dallas, Texas in Fahrenheit?"}, - { - "role": "assistant", - "tool_calls": [ - { - "id": "123456789", - "type": "function", - "function": { - "name": "get_current_weather", - "arguments": '{"city": "Dallas", "state": "TX", ' - '"unit": "fahrenheit"}', - }, - } - ], - }, - {"role": "tool", "tool_call_id": "123456789", "content": "98"}, -] - -WEATHER_FORECAST_TOOL_CHOICE: ChatCompletionNamedToolChoiceParam = { - "function": {"name": "get_n_day_weather_forecast"}, - "type": "function", -} - - -@pytest.mark.openai -class TestAsyncClientToolCalling: - @pytest.fixture(scope="class") - def client(self, server): - return server.get_async_client() - - def validate_tool_calls_present( - self, tool_calls: Optional[List[ChatCompletionMessageToolCall]], skip_id=False - ): - assert tool_calls is not None - assert len(tool_calls) == 1 - assert tool_calls[0].type == "function" - assert tool_calls[0].function is not None - assert isinstance(tool_calls[0].id, str) - if not skip_id: - assert len(tool_calls[0].id) >= 9 - - def validate_weather_tool_arguments(self, parsed_arguments: Dict): - assert isinstance(parsed_arguments, Dict) - assert isinstance(parsed_arguments.get("city"), str) - assert isinstance(parsed_arguments.get("state"), str) - assert isinstance(parsed_arguments.get("unit"), str) - assert parsed_arguments.get("city") == "Dallas" - assert parsed_arguments.get("state") in ("TX", "Texas") - assert parsed_arguments.get("unit") == "fahrenheit" - - def validate_weather_forcast_tool_arguments(self, parsed_arguments: Dict): - assert isinstance(parsed_arguments, Dict) - assert isinstance(parsed_arguments.get("city"), str) - assert isinstance(parsed_arguments.get("state"), str) - assert isinstance(parsed_arguments.get("unit"), str) - assert isinstance(parsed_arguments.get("num_days"), int) - assert parsed_arguments.get("city") == "Dallas" - assert parsed_arguments.get("state") in ("TX", "Texas") - assert parsed_arguments.get("unit") == "fahrenheit" - - @pytest.mark.asyncio - async def test_tool_call_and_choice( - self, client: openai.AsyncOpenAI, model: str, backend: str - ): - # FIXME: [TRI-992] Maybe an issue on TRT-LLM but unverified. - if backend == "vllm" and model == "mistral-nemo-instruct-2407": - pytest.skip( - reason="Mistral model tool calling is not triggered with tool_choice=auto (default)." - ) - - chat_completion = await client.chat.completions.create( - messages=MESSAGES_ASKING_FOR_TOOLS, - temperature=0, - max_completion_tokens=128, - model=model, - tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], - logprobs=False, - ) - - choice = chat_completion.choices[0] - stop_reason = chat_completion.choices[0].finish_reason - tool_calls = chat_completion.choices[0].message.tool_calls - - # make sure a tool call is present - self.validate_tool_calls_present(tool_calls) - assert stop_reason == "tool_calls" - - # make sure the weather tool was called (classic example) with arguments - assert tool_calls[0].function.name == WEATHER_TOOL["function"]["name"] - assert tool_calls[0].function.arguments is not None - assert isinstance(tool_calls[0].function.arguments, str) - - # make sure the arguments parse properly - parsed_arguments = json.loads(tool_calls[0].function.arguments) - self.validate_weather_tool_arguments(parsed_arguments) - - function_name: Optional[str] = None - function_args_str: str = "" - tool_call_id: Optional[str] = None - role_name: Optional[str] = None - finish_reason_count: int = 0 - - # make the same request, streaming - stream = await client.chat.completions.create( - model=model, - messages=MESSAGES_ASKING_FOR_TOOLS, - temperature=0, - max_completion_tokens=128, - tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], - logprobs=False, - stream=True, - ) - - async for chunk in stream: - assert chunk.choices[0].index == 0 - - if chunk.choices[0].finish_reason: - finish_reason_count += 1 - assert chunk.choices[0].finish_reason == "tool_calls" - - # if a role is being streamed make sure it wasn't already set to - # something else - if chunk.choices[0].delta.role: - assert not role_name or role_name == "assistant" - role_name = "assistant" - - # if a tool call is streamed make sure there's exactly one - # (based on the request parameters - streamed_tool_calls = chunk.choices[0].delta.tool_calls - - if streamed_tool_calls and len(streamed_tool_calls) > 0: - assert len(streamed_tool_calls) == 1 - tool_call = streamed_tool_calls[0] - - # if a tool call ID is streamed, make sure one hasn't been already - if tool_call.id: - assert not tool_call_id - tool_call_id = tool_call.id - - # if parts of the function start being streamed - if tool_call.function: - # if the function name is defined, set it. it should be streamed - # IN ENTIRETY, exactly one time. - if tool_call.function.name: - assert function_name is None - assert isinstance(tool_call.function.name, str) - function_name = tool_call.function.name - if tool_call.function.arguments: - assert isinstance(tool_call.function.arguments, str) - function_args_str += tool_call.function.arguments - - assert finish_reason_count == 1 - assert role_name == "assistant" - assert isinstance(tool_call_id, str) and (len(tool_call_id) >= 9) - - # validate the name and arguments - assert function_name == WEATHER_TOOL["function"]["name"] - assert function_name == tool_calls[0].function.name - assert isinstance(function_args_str, str) - - # validate arguments - streamed_args = json.loads(function_args_str) - self.validate_weather_tool_arguments(streamed_args) - - # make sure everything matches non-streaming except for ID - assert function_name == tool_calls[0].function.name - assert choice.message.role == role_name - assert choice.message.tool_calls[0].function.name == function_name - - # compare streamed with non-streamed args Dict-wise, not string-wise - # because character-to-character comparison might not work e.g. the tool - # call parser adding extra spaces or something like that. we care about the - # dicts matching not byte-wise match - assert parsed_arguments == streamed_args - - @pytest.mark.asyncio - async def test_tool_call_with_reply_response( - self, client: openai.AsyncOpenAI, model: str, backend: str - ): - chat_completion = await client.chat.completions.create( - messages=MESSAGES_WITH_TOOL_RESPONSE, - temperature=0, - max_completion_tokens=128, - model=model, - tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], - logprobs=False, - seed=0, - ) - - choice = chat_completion.choices[0] - - assert choice.finish_reason != "tool_calls" # "stop" - assert choice.message.role == "assistant" - assert choice.message.tool_calls is None or len(choice.message.tool_calls) == 0 - assert choice.message.content is not None - - stream = await client.chat.completions.create( - messages=MESSAGES_WITH_TOOL_RESPONSE, - temperature=0, - max_completion_tokens=128, - model=model, - tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], - logprobs=False, - stream=True, - seed=0, - ) - - chunks: List[str] = [] - finish_reason_count = 0 - role_sent: bool = False - - async for chunk in stream: - delta = chunk.choices[0].delta - - if delta.role: - assert not role_sent - assert delta.role == "assistant" - role_sent = True - - if delta.content: - chunks.append(delta.content) - - if chunk.choices[0].finish_reason is not None: - finish_reason_count += 1 - assert chunk.choices[0].finish_reason == choice.finish_reason - - assert not delta.tool_calls or len(delta.tool_calls) == 0 - - assert role_sent - assert finish_reason_count == 1 - assert len(chunks) - - # validate if steaming and non-streaming generates the same content - assert "".join(chunks) == choice.message.content - - @pytest.mark.asyncio - async def test_tool_call_with_named_tool_choice( - self, client: openai.AsyncOpenAI, model: str - ): - chat_completion = await client.chat.completions.create( - messages=MESSAGES_ASKING_FOR_TOOLS, - temperature=0, - max_completion_tokens=128, - model=model, - tool_choice=WEATHER_FORECAST_TOOL_CHOICE, - tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], - logprobs=False, - ) - - choice = chat_completion.choices[0] - stop_reason = chat_completion.choices[0].finish_reason - tool_calls = chat_completion.choices[0].message.tool_calls - - # make sure a tool call is present - self.validate_tool_calls_present(tool_calls, skip_id=True) - assert stop_reason != "tool_calls" - - # make sure the weather tool was called (classic example) with arguments - assert tool_calls[0].function.name == WEATHER_FORECAST_TOOL["function"]["name"] - assert tool_calls[0].function.arguments is not None - assert isinstance(tool_calls[0].function.arguments, str) - - # make sure the arguments parse properly - parsed_arguments = json.loads(tool_calls[0].function.arguments) - self.validate_weather_forcast_tool_arguments(parsed_arguments) - - function_name: Optional[str] = None - function_args_str: str = "" - tool_call_id: Optional[str] = None - role_name: Optional[str] = None - finish_reason_count: int = 0 - - # make the same request, streaming - stream = await client.chat.completions.create( - model=model, - messages=MESSAGES_ASKING_FOR_TOOLS, - temperature=0, - max_completion_tokens=128, - tool_choice=WEATHER_FORECAST_TOOL_CHOICE, - tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], - logprobs=False, - stream=True, - ) - - async for chunk in stream: - assert chunk.choices[0].index == 0 - - if chunk.choices[0].finish_reason: - finish_reason_count += 1 - assert chunk.choices[0].finish_reason != "tool_calls" - - # if a role is being streamed make sure it wasn't already set to - # something else - if chunk.choices[0].delta.role: - assert not role_name or role_name == "assistant" - role_name = "assistant" - - # if a tool call is streamed make sure there's exactly one - # (based on the request parameters - streamed_tool_calls = chunk.choices[0].delta.tool_calls - - if streamed_tool_calls and len(streamed_tool_calls) > 0: - assert len(streamed_tool_calls) == 1 - tool_call = streamed_tool_calls[0] - - # if a tool call ID is streamed, make sure one hasn't been already - if tool_call.id: - assert not tool_call_id - tool_call_id = tool_call.id - - # if parts of the function start being streamed - if tool_call.function: - # if the function name is defined, set it. it should be streamed - # IN ENTIRETY, exactly one time. - if tool_call.function.name: - assert isinstance(tool_call.function.name, str) - function_name = tool_call.function.name - if tool_call.function.arguments: - assert isinstance(tool_call.function.arguments, str) - function_args_str += tool_call.function.arguments - - assert finish_reason_count == 1 - assert role_name == "assistant" - - # validate the name and arguments - assert function_name == WEATHER_FORECAST_TOOL["function"]["name"] - assert function_name == tool_calls[0].function.name - assert isinstance(function_args_str, str) - - # validate arguments - streamed_args = json.loads(function_args_str) - self.validate_weather_forcast_tool_arguments(streamed_args) - - # make sure everything matches non-streaming except for ID - assert function_name == tool_calls[0].function.name - assert choice.message.role == role_name - assert choice.message.tool_calls[0].function.name == function_name - - @pytest.mark.asyncio - async def test_tool_call_with_required_tool_choice( - self, client: openai.AsyncOpenAI, model: str - ): - chat_completion = await client.chat.completions.create( - messages=MESSAGES_ASKING_FOR_TOOLS, - temperature=0, - max_completion_tokens=128, - model=model, - tool_choice="required", - tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], - logprobs=False, - ) - - choice = chat_completion.choices[0] - stop_reason = chat_completion.choices[0].finish_reason - tool_calls = chat_completion.choices[0].message.tool_calls - - # make sure a tool call is present - self.validate_tool_calls_present(tool_calls, skip_id=True) - assert stop_reason != "tool_calls" - - # make sure the weather tool was called (classic example) with arguments - assert tool_calls[0].function.name == WEATHER_TOOL["function"]["name"] - assert tool_calls[0].function.arguments is not None - assert isinstance(tool_calls[0].function.arguments, str) - - # make sure the arguments parse properly - parsed_arguments = json.loads(tool_calls[0].function.arguments) - self.validate_weather_tool_arguments(parsed_arguments) - - function_name: Optional[str] = None - function_args_str: str = "" - tool_call_id: Optional[str] = None - role_name: Optional[str] = None - finish_reason_count: int = 0 - - # make the same request, streaming - stream = await client.chat.completions.create( - model=model, - messages=MESSAGES_ASKING_FOR_TOOLS, - temperature=0, - max_completion_tokens=128, - tool_choice="required", - tools=[WEATHER_TOOL, WEATHER_FORECAST_TOOL], - logprobs=False, - stream=True, - ) - - async for chunk in stream: - assert chunk.choices[0].index == 0 - - if chunk.choices[0].finish_reason: - finish_reason_count += 1 - assert chunk.choices[0].finish_reason != "tool_calls" - - # if a role is being streamed make sure it wasn't already set to - # something else - if chunk.choices[0].delta.role: - assert not role_name or role_name == "assistant" - role_name = "assistant" - - # if a tool call is streamed make sure there's exactly one - # (based on the request parameters - streamed_tool_calls = chunk.choices[0].delta.tool_calls - - if streamed_tool_calls and len(streamed_tool_calls) > 0: - assert len(streamed_tool_calls) == 1 - tool_call = streamed_tool_calls[0] - - # if a tool call ID is streamed, make sure one hasn't been already - if tool_call.id: - assert not tool_call_id - tool_call_id = tool_call.id - - # if parts of the function start being streamed - if tool_call.function: - # if the function name is defined, set it. it should be streamed - # IN ENTIRETY, exactly one time. - if tool_call.function.name: - assert isinstance(tool_call.function.name, str) - function_name = tool_call.function.name - if tool_call.function.arguments: - assert isinstance(tool_call.function.arguments, str) - function_args_str += tool_call.function.arguments - - assert finish_reason_count == 1 - assert role_name == "assistant" - - # validate the name and arguments - assert function_name == WEATHER_TOOL["function"]["name"] - assert function_name == tool_calls[0].function.name - assert isinstance(function_args_str, str) - - # validate arguments - streamed_args = json.loads(function_args_str) - self.validate_weather_tool_arguments(streamed_args) - - # make sure everything matches non-streaming except for ID - assert function_name == tool_calls[0].function.name - assert choice.message.role == role_name - assert choice.message.tool_calls[0].function.name == function_name - - @pytest.mark.asyncio - async def test_inconsistent_tool_choice_and_tools( - self, client: openai.AsyncOpenAI, model: str - ): - # tool choice function but the tools are empty - with pytest.raises(openai.BadRequestError): - await client.chat.completions.create( - messages=MESSAGES_ASKING_FOR_TOOLS, - temperature=0, - max_completion_tokens=128, - model=model, - tool_choice=WEATHER_FORECAST_TOOL_CHOICE, - logprobs=False, - ) - # tool choice function that is not provided in the tools - with pytest.raises(openai.BadRequestError): - await client.chat.completions.create( - messages=MESSAGES_ASKING_FOR_TOOLS, - temperature=0, - max_completion_tokens=128, - model=model, - tool_choice=WEATHER_FORECAST_TOOL_CHOICE, - tools=[WEATHER_TOOL], - logprobs=False, - ) - - # tool choice required but tools is empty - with pytest.raises(openai.BadRequestError): - await client.chat.completions.create( - messages=MESSAGES_ASKING_FOR_TOOLS, - temperature=0, - max_completion_tokens=128, - model=model, - tool_choice="required", - tools=[], - logprobs=False, - ) diff --git a/python/openai/tests/utils.py b/python/openai/tests/utils.py index c73453ee90..fdffcc5ea9 100644 --- a/python/openai/tests/utils.py +++ b/python/openai/tests/utils.py @@ -1,4 +1,4 @@ -# Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -27,7 +27,6 @@ import os import subprocess import sys -import threading import time from pathlib import Path from typing import Dict, List, Optional @@ -42,15 +41,9 @@ # TODO: Cleanup, refactor, mock, etc. -def setup_server( - model_repository: str, - model_control_mode: tritonserver.ModelControlMode = tritonserver.ModelControlMode.NONE, - load_models: Optional[List[str]] = None, -): +def setup_server(model_repository: str): server: tritonserver.Server = tritonserver.Server( model_repository=model_repository, - model_control_mode=model_control_mode, - startup_models=load_models or [], log_verbose=0, log_info=True, log_warn=True, @@ -59,17 +52,9 @@ def setup_server( return server -def setup_fastapi_app( - tokenizer: str, - server: tritonserver.Server, - backend: str, - default_max_tokens: int = 16, -): +def setup_fastapi_app(tokenizer: str, server: tritonserver.Server, backend: str): engine: TritonLLMEngine = TritonLLMEngine( - server=server, - tokenizer=tokenizer, - backend=backend, - default_max_tokens=default_max_tokens, + server=server, tokenizer=tokenizer, backend=backend ) frontend: FastApiFrontend = FastApiFrontend(engine=engine) return frontend.app @@ -78,7 +63,7 @@ def setup_fastapi_app( # Heavily inspired by vLLM's test infrastructure class OpenAIServer: API_KEY = "EMPTY" # Triton's OpenAI server does not need API key - START_TIMEOUT = 240 # wait for server to start for up to 240 seconds, mistral model takes longer time to start + START_TIMEOUT = 120 # wait for server to start for up to 120 seconds def __init__( self, @@ -100,29 +85,13 @@ def __init__( ["python3", script_path] + cli_args, env=env, stdout=sys.stdout, - stderr=subprocess.PIPE, # Capture stderr - text=True, + stderr=sys.stderr, ) - self.stderr_lines = [] - threading.Thread(target=self._read_stderr, daemon=True).start() # Wait until health endpoint is responsive self._wait_for_server( url=self.url_for("health", "ready"), timeout=self.START_TIMEOUT ) - def _read_stderr(self): - """Read stderr and print to console in real-time. Continues throughout server lifecycle.""" - try: - if self.proc.stderr: - for line in iter(self.proc.stderr.readline, ""): - self.stderr_lines.append(line.rstrip("\n\r")) - sys.stderr.write(line) - sys.stderr.flush() - except (OSError, ValueError, BrokenPipeError) as exc: - # Ignore expected errors during process shutdown, but log for debugging. - sys.stderr.write(f"[OpenAIServer] Error while reading stderr: {exc}\n") - sys.stderr.flush() - def __enter__(self): return self @@ -144,31 +113,11 @@ def _wait_for_server(self, *, url: str, timeout: float): except Exception as err: result = self.proc.poll() if result is not None and result != 0: - stderr_text = ( - "\n".join(self.stderr_lines) - if self.stderr_lines - else "No stderr output" - ) - error = RuntimeError( - f"Server exited unexpectedly with return code {result}.\n" - f"Stderr output:\n{stderr_text}" - ) - error.stderr_lines = list(self.stderr_lines) - raise error from err + raise RuntimeError("Server exited unexpectedly.") from err time.sleep(0.5) if time.time() - start > timeout: - stderr_text = ( - "\n".join(self.stderr_lines) - if self.stderr_lines - else "No stderr output" - ) - error = RuntimeError( - f"Server failed to start in time.\n" - f"Stderr output:\n{stderr_text}" - ) - error.stderr_lines = list(self.stderr_lines) - raise error from err + raise RuntimeError("Server failed to start in time.") from err @property def url_root(self) -> str: diff --git a/python/openai/tests/vllm_embedding_models/all-MiniLM-L6-v2/1/model.json b/python/openai/tests/vllm_embedding_models/all-MiniLM-L6-v2/1/model.json deleted file mode 100644 index 2ad058c2e5..0000000000 --- a/python/openai/tests/vllm_embedding_models/all-MiniLM-L6-v2/1/model.json +++ /dev/null @@ -1 +0,0 @@ -{"model": "sentence-transformers/all-MiniLM-L6-v2", "gpu_memory_utilization": 0.5} diff --git a/python/openai/tests/vllm_mistral_models/mistral-nemo-instruct-2407/1/model.json b/python/openai/tests/vllm_mistral_models/mistral-nemo-instruct-2407/1/model.json deleted file mode 100644 index b7ce0ee199..0000000000 --- a/python/openai/tests/vllm_mistral_models/mistral-nemo-instruct-2407/1/model.json +++ /dev/null @@ -1 +0,0 @@ -{"model": "mistralai/Mistral-Nemo-Instruct-2407", "gpu_memory_utilization": 0.9} \ No newline at end of file diff --git a/python/openai/tests/vllm_mistral_models/mistral-nemo-instruct-2407/config.pbtxt b/python/openai/tests/vllm_mistral_models/mistral-nemo-instruct-2407/config.pbtxt deleted file mode 100644 index 39b3c48edb..0000000000 --- a/python/openai/tests/vllm_mistral_models/mistral-nemo-instruct-2407/config.pbtxt +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -backend: "vllm" -instance_group [{kind: KIND_MODEL}] diff --git a/python/openai/tests/vllm_models/llama-3.1-8b-instruct/1/model.json b/python/openai/tests/vllm_models/llama-3.1-8b-instruct/1/model.json index df85a05da0..cb9b14c765 100644 --- a/python/openai/tests/vllm_models/llama-3.1-8b-instruct/1/model.json +++ b/python/openai/tests/vllm_models/llama-3.1-8b-instruct/1/model.json @@ -1 +1 @@ -{"model": "meta-llama/Meta-Llama-3.1-8B-Instruct", "gpu_memory_utilization": 0.9} +{"model": "meta-llama/Meta-Llama-3.1-8B-Instruct", "disable_log_requests": true, "gpu_memory_utilization": 0.9} diff --git a/qa/L0_backend_bls/test.sh b/qa/L0_backend_bls/test.sh index 9e27f50870..6210e7654c 100755 --- a/qa/L0_backend_bls/test.sh +++ b/qa/L0_backend_bls/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -45,7 +45,7 @@ apt update -q=2 \ && . /etc/os-release \ && echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $UBUNTU_CODENAME main" | tee /etc/apt/sources.list.d/kitware.list >/dev/null \ && apt-get update -q=2 \ - && apt-get install -y --no-install-recommends cmake=4.0.3* cmake-data=4.0.3* \ + && apt-get install -y --no-install-recommends cmake=3.28.3* cmake-data=3.28.3* \ rapidjson-dev cmake --version @@ -57,7 +57,6 @@ git clone --single-branch --depth=1 -b $TRITON_BACKEND_REPO_TAG \ (cd backend/examples/backends/bls && mkdir build && cd build && - export CMAKE_POLICY_VERSION_MINIMUM=3.5 && cmake -DCMAKE_INSTALL_PREFIX:PATH=`pwd`/install \ -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ -DTRITON_BACKEND_REPO_TAG=${TRITON_BACKEND_REPO_TAG} \ diff --git a/qa/L0_backend_identity/identity_test.py b/qa/L0_backend_identity/identity_test.py index b96649241d..a607e4189b 100755 --- a/qa/L0_backend_identity/identity_test.py +++ b/qa/L0_backend_identity/identity_test.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -30,7 +30,6 @@ import sys from builtins import range -import ml_dtypes import numpy as np import requests as httpreq import tritonclient.grpc as grpcclient @@ -202,8 +201,8 @@ ("identity_nobatch_int8", np.int8, [0]), ("identity_nobatch_int8", np.int8, [7]), ("identity_bytes", object, [1, 1]), - ("identity_bf16", ml_dtypes.bfloat16, [1, 0]), - ("identity_bf16", ml_dtypes.bfloat16, [1, 5]) + ("identity_bf16", np.float32, [1, 0]), + ("identity_bf16", np.float32, [1, 5]) ): # yapf: enable if np_dtype != object: @@ -235,20 +234,40 @@ print("error: expected 'OUTPUT0'") sys.exit(1) - if output_data.dtype != input_data.dtype: - print( - "error: expected output dtype {} to match input dtype {} for {}".format( - output_data.dtype, input_data.dtype, model_name + if model_name == "identity_bf16": + if input_data.shape != output_data.shape: + print( + "error: expected output shape {} to match input shape {}".format( + output_data.shape, input_data.shape + ) ) - ) - sys.exit(1) - if not np.array_equal(output_data, input_data): - print( - "error: expected output {} to match input {} for {}".format( - output_data, input_data, model_name + sys.exit(1) + for input, output in zip( + np.nditer(input_data, flags=["refs_ok", "zerosize_ok"], order="C"), + np.nditer(output_data, flags=["refs_ok", "zerosize_ok"], order="C"), + ): + if input.tobytes()[2:4] != output.tobytes()[2:4]: + print( + "error: expected low-order bits of output {} to match low-order bits of input {}".format( + output, input + ) + ) + sys.exit(1) + if output.tobytes()[0:2] != b"\x00\x00": + print( + "error: expected output {} to have all-zero high-order bits, got {}".format( + output, output.tobytes()[0:2] + ) + ) + sys.exit(1) + else: + if not np.array_equal(output_data, input_data): + print( + "error: expected output {} to match input {}".format( + output_data, input_data + ) ) - ) - sys.exit(1) + sys.exit(1) # Make sure response parameters are correct response = results.get_response() diff --git a/qa/L0_backend_onnxruntime/test.py b/qa/L0_backend_onnxruntime/test.py deleted file mode 100755 index 7575f6d0f5..0000000000 --- a/qa/L0_backend_onnxruntime/test.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import os -import unittest - -import ml_dtypes -import numpy as np -import tritonclient.grpc as grpcclient -import tritonclient.http as httpclient - - -class BFloat16Test(unittest.TestCase): - def setUp(self): - self.protocol = os.environ.get("CLIENT_TYPE", "http") - if self.protocol == "http": - self.client_ = httpclient.InferenceServerClient("localhost:8000") - self.client_module_ = httpclient - else: - self.client_ = grpcclient.InferenceServerClient("localhost:8001") - self.client_module_ = grpcclient - self.model_name_ = "onnx_bf16_bf16_bf16" - # Model dims are [-1, 16]: dynamic batch dim plus inner dim of 16. - self.inner_dim_ = 16 - - def _infer_bf16(self, input0_data, input1_data): - """Helper to run BF16 inference and return the output numpy arrays.""" - input0 = self.client_module_.InferInput( - "INPUT0", list(input0_data.shape), "BF16" - ) - input1 = self.client_module_.InferInput( - "INPUT1", list(input1_data.shape), "BF16" - ) - input0.set_data_from_numpy(input0_data) - input1.set_data_from_numpy(input1_data) - - results = self.client_.infer(self.model_name_, [input0, input1]) - return results.as_numpy("OUTPUT0"), results.as_numpy("OUTPUT1") - - def test_bf16_add_sub_variants(self): - """Run BF16 add/sub across multiple cases batched in a single request: - zeros, negatives, large, small, cancellation, and identical.""" - cases = [ - (0.0, 0.0), - (-1.5, 3.5), - (100.0, 200.0), - (1e-2, 1e-2), - (1.0, -1.0), - (2.0, 2.0), - ] - batch_size = len(cases) - input0_data = np.empty((batch_size, self.inner_dim_), dtype=ml_dtypes.bfloat16) - input1_data = np.empty((batch_size, self.inner_dim_), dtype=ml_dtypes.bfloat16) - for i, (v0, v1) in enumerate(cases): - input0_data[i, :] = v0 - input1_data[i, :] = v1 - - output0, output1 = self._infer_bf16(input0_data, input1_data) - self.assertEqual(output0.dtype, ml_dtypes.bfloat16) - self.assertEqual(output1.dtype, ml_dtypes.bfloat16) - np.testing.assert_array_equal(output0, input0_data + input1_data) - np.testing.assert_array_equal(output1, input0_data - input1_data) - - -if __name__ == "__main__": - unittest.main() diff --git a/qa/L0_backend_python/common.sh b/qa/L0_backend_python/common.sh index 8b447b1713..70fb3cc081 100755 --- a/qa/L0_backend_python/common.sh +++ b/qa/L0_backend_python/common.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -51,7 +51,7 @@ install_build_deps_apt() { && . /etc/os-release \ && echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $UBUNTU_CODENAME main" | tee /etc/apt/sources.list.d/kitware.list >/dev/null \ && apt-get update -q=2 \ - && apt-get install -y --no-install-recommends cmake=4.0.3* cmake-data=4.0.3* + && apt-get install -y --no-install-recommends cmake=3.28.3* cmake-data=3.28.3* } install_build_deps_yum() { @@ -59,7 +59,7 @@ install_build_deps_yum() { } install_build_deps() { - if [[ ${TRITON_RHEL} -eq "1" ]] && grep -qE 'rhel|centos|fedora' /etc/os-release; then + if [[ ${TRITON_RHEL} -eq "1" ]]; then install_build_deps_yum else install_build_deps_apt @@ -85,22 +85,7 @@ create_conda_env_with_specified_path() { create_python_backend_stub() { rm -rf python_backend git clone ${TRITON_REPO_ORGANIZATION}/python_backend -b $PYTHON_BACKEND_REPO_TAG - CUDA_PATH=$(readlink -f /usr/local/cuda) - export CMAKE_POLICY_VERSION_MINIMUM=3.5 - (cd python_backend/ \ - && mkdir builddir \ - && cd builddir \ - && export CMAKE_POLICY_VERSION_MINIMUM=3.5 \ - && cmake \ - -DCMAKE_CUDA_COMPILER=$CUDA_PATH/bin/nvcc \ - -DCMAKE_INCLUDE_PATH:STRING=/usr/include \ - -DCUDAToolkit_ROOT=$CUDA_PATH \ - -DPYBIND11_PYTHON_VERSION=$PY_VERSION \ - -DTRITON_BACKEND_REPO_TAG=$TRITON_BACKEND_REPO_TAG \ - -DTRITON_COMMON_REPO_TAG=$TRITON_COMMON_REPO_TAG \ - -DTRITON_CORE_REPO_TAG=$TRITON_CORE_REPO_TAG \ - -DTRITON_ENABLE_GPU=ON \ - -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ - -S ../ \ - && cmake --build . --target triton-python-backend-stub -j18) + (cd python_backend/ && mkdir builddir && cd builddir && \ + cmake -DTRITON_ENABLE_GPU=ON -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} -DTRITON_BACKEND_REPO_TAG=$TRITON_BACKEND_REPO_TAG -DTRITON_COMMON_REPO_TAG=$TRITON_COMMON_REPO_TAG -DTRITON_CORE_REPO_TAG=$TRITON_CORE_REPO_TAG -DPYBIND11_PYTHON_VERSION=$PY_VERSION ../ && \ + make -j18 triton-python-backend-stub) } diff --git a/qa/L0_backend_python/decoupled/decoupled_test.py b/qa/L0_backend_python/decoupled/decoupled_test.py index d3442e03a0..45ce370fb1 100755 --- a/qa/L0_backend_python/decoupled/decoupled_test.py +++ b/qa/L0_backend_python/decoupled/decoupled_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -46,34 +46,6 @@ _tritonserver_ipaddr = os.environ.get("TRITONSERVER_IPADDR", "localhost") -def prepare_decoupled_bls_cancel_inputs(input_value, max_sum_value, ignore_cancel): - input_data = np.array([input_value], dtype=np.int32) - max_sum_data = np.array([max_sum_value], dtype=np.int32) - ignore_cancel_data = np.array([ignore_cancel], dtype=np.bool_) - inputs = [ - grpcclient.InferInput( - "INPUT", - input_data.shape, - np_to_triton_dtype(input_data.dtype), - ), - grpcclient.InferInput( - "MAX_SUM", - max_sum_data.shape, - np_to_triton_dtype(max_sum_data.dtype), - ), - grpcclient.InferInput( - "IGNORE_CANCEL", - ignore_cancel_data.shape, - np_to_triton_dtype(ignore_cancel_data.dtype), - ), - ] - inputs[0].set_data_from_numpy(input_data) - inputs[1].set_data_from_numpy(max_sum_data) - inputs[2].set_data_from_numpy(ignore_cancel_data) - - return inputs - - class UserData: def __init__(self): self._completed_requests = queue.Queue() @@ -352,171 +324,6 @@ def test_decoupled_execute_cancel(self): self.assertIn("[execute_cancel] Request not cancelled at 1.0 s", log_text) self.assertIn("[execute_cancel] Request cancelled at ", log_text) - def test_decoupled_bls_cancel(self): - model_names = ["decoupled_bls_cancel", "decoupled_bls_async_cancel"] - input_value = 1 - max_sum_value = 10 - ignore_cancel = False - user_data = UserData() - for model_name in model_names: - with self._shm_leak_detector.Probe() as shm_probe: - with grpcclient.InferenceServerClient( - f"{_tritonserver_ipaddr}:8001" - ) as client: - client.start_stream(callback=partial(callback, user_data)) - inputs = prepare_decoupled_bls_cancel_inputs( - input_value=input_value, - max_sum_value=max_sum_value, - ignore_cancel=ignore_cancel, - ) - client.async_stream_infer(model_name, inputs) - - # Check the results of the decoupled model using BLS - def check_result(result): - # Make sure the result is not an exception - self.assertIsNot(type(result), InferenceServerException) - is_cancelled = result.as_numpy("IS_CANCELLED") - self.assertTrue( - is_cancelled[0], - "error: expected the request to be cancelled", - ) - - max_sum_data = np.array([max_sum_value], dtype=np.int32) - sum_data = result.as_numpy("SUM") - self.assertIsNotNone(sum_data, "error: expected 'SUM'") - self.assertTrue( - np.array_equal(sum_data, max_sum_data), - "error: expected output {} to match input {}".format( - sum_data, max_sum_data - ), - ) - - result = user_data._completed_requests.get() - check_result(result) - - def test_decoupled_bls_ignore_cancel(self): - model_names = ["decoupled_bls_cancel", "decoupled_bls_async_cancel"] - input_value = 1 - max_sum_value = 10 - ignore_cancel = True - user_data = UserData() - for model_name in model_names: - with self._shm_leak_detector.Probe() as shm_probe: - with grpcclient.InferenceServerClient( - f"{_tritonserver_ipaddr}:8001" - ) as client: - client.start_stream(callback=partial(callback, user_data)) - inputs = prepare_decoupled_bls_cancel_inputs( - input_value=input_value, - max_sum_value=max_sum_value, - ignore_cancel=ignore_cancel, - ) - client.async_stream_infer(model_name, inputs) - - # Check the results of the decoupled model using BLS - def check_result(result): - # Make sure the result is not an exception - self.assertIsNot(type(result), InferenceServerException) - is_cancelled = result.as_numpy("IS_CANCELLED") - self.assertFalse( - is_cancelled[0], - "error: expected the request not being cancelled", - ) - - max_sum_data = np.array([max_sum_value], dtype=np.int32) - sum_data = result.as_numpy("SUM") - self.assertIsNotNone(sum_data, "error: expected 'SUM'") - self.assertTrue( - sum_data > max_sum_data, - "error: expected sum_data {} to be greater than max_sum_data {}".format( - sum_data, max_sum_data - ), - ) - - result = user_data._completed_requests.get() - check_result(result) - - def test_decoupled_bls_cancel_after_cancellation(self): - model_name = "decoupled_bls_cancel_after_complete" - input_value = 1 - max_sum_value = 10 - ignore_cancel = False - user_data = UserData() - with self._shm_leak_detector.Probe() as shm_probe: - with grpcclient.InferenceServerClient( - f"{_tritonserver_ipaddr}:8001" - ) as client: - client.start_stream(callback=partial(callback, user_data)) - inputs = prepare_decoupled_bls_cancel_inputs( - input_value=input_value, - max_sum_value=max_sum_value, - ignore_cancel=ignore_cancel, - ) - client.async_stream_infer(model_name, inputs) - - # Check the results of the decoupled model using BLS - def check_result(result): - # Make sure the result is not an exception - self.assertIsNot(type(result), InferenceServerException) - is_cancelled = result.as_numpy("IS_CANCELLED") - self.assertTrue( - is_cancelled[0], "error: expected the request to be cancelled" - ) - - max_sum_data = np.array([max_sum_value], dtype=np.int32) - sum_data = result.as_numpy("SUM") - self.assertIsNotNone(sum_data, "error: expected 'SUM'") - self.assertTrue( - np.array_equal(sum_data, max_sum_data), - "error: expected output {} to match input {}".format( - sum_data, max_sum_data - ), - ) - - result = user_data._completed_requests.get() - check_result(result) - - def test_decoupled_bls_cancel_after_completion(self): - model_name = "decoupled_bls_cancel_after_complete" - input_value = 1 - max_sum_value = 25 - ignore_cancel = False - user_data = UserData() - with self._shm_leak_detector.Probe() as shm_probe: - with grpcclient.InferenceServerClient( - f"{_tritonserver_ipaddr}:8001" - ) as client: - client.start_stream(callback=partial(callback, user_data)) - inputs = prepare_decoupled_bls_cancel_inputs( - input_value=input_value, - max_sum_value=max_sum_value, - ignore_cancel=ignore_cancel, - ) - client.async_stream_infer(model_name, inputs) - - # Check the results of the decoupled model using BLS - def check_result(result): - # Make sure the result is not an exception - self.assertIsNot(type(result), InferenceServerException) - is_cancelled = result.as_numpy("IS_CANCELLED") - self.assertFalse( - is_cancelled[0], - "error: expected the request not being cancelled", - ) - - max_sum_data = np.array([max_sum_value], dtype=np.int32) - sum_data = result.as_numpy("SUM") - self.assertIsNotNone(sum_data, "error: expected 'SUM'") - self.assertTrue( - sum_data < max_sum_data, - "error: expected sum_data {} to be lesser than max_sum_data {}".format( - sum_data, max_sum_data - ), - ) - - result = user_data._completed_requests.get() - check_result(result) - def test_decoupled_raise_exception(self): # The decoupled_raise_exception model raises an exception for the request. # This test case is making sure that repeated exceptions are properly handled. diff --git a/qa/L0_backend_python/decoupled/models/decoupled_bls_async_cancel/1/model.py b/qa/L0_backend_python/decoupled/models/decoupled_bls_async_cancel/1/model.py deleted file mode 100644 index d84221e7e2..0000000000 --- a/qa/L0_backend_python/decoupled/models/decoupled_bls_async_cancel/1/model.py +++ /dev/null @@ -1,93 +0,0 @@ -# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import asyncio - -import numpy as np -import triton_python_backend_utils as pb_utils - - -class TritonPythonModel: - """ - This model sends a decoupled bls inference request to 'response_sender_until_cancelled' - model, and sums up its responses. - Once the MAX_SUM is reached, the model will call the response iterator's - cancel() method to cancel the response stream. - If the IGNORE_CANCEL is set to True, the 'response_sender_until_cancelled' model will not hornor - the request cancellation and keep sending the output to the model. - The number of total responses should not reach MAX_RESPONSE_COUNT. - """ - - async def execute(self, requests): - max_sum = ( - pb_utils.get_input_tensor_by_name(requests[0], "MAX_SUM").as_numpy().flat[0] - ) - input = pb_utils.get_input_tensor_by_name(requests[0], "INPUT") - ignore_cancel = pb_utils.get_input_tensor_by_name(requests[0], "IGNORE_CANCEL") - delay = pb_utils.Tensor("DELAY", np.array([50], dtype=np.int32)) - max_response_count = pb_utils.Tensor( - "MAX_RESPONSE_COUNT", np.array([20], dtype=np.int32) - ) - - infer_request = pb_utils.InferenceRequest( - model_name="response_sender_until_cancelled", - inputs=[input, max_response_count, delay, ignore_cancel], - requested_output_names=["OUTPUT"], - ) - - response_stream = await infer_request.async_exec(decoupled=True) - - is_cancelled = False - error = None - response_sum = 0 - for infer_response in response_stream: - if infer_response.has_error(): - if infer_response.error().code() == pb_utils.TritonError.CANCELLED: - is_cancelled = True - else: - error = infer_response.error() - break - - out = pb_utils.get_output_tensor_by_name( - infer_response, "OUTPUT" - ).as_numpy()[0] - - response_sum += out - if response_sum >= max_sum: - response_stream.cancel() - - responses = [ - pb_utils.InferenceResponse( - output_tensors=[ - pb_utils.Tensor("SUM", np.array([response_sum], dtype=np.int32)), - pb_utils.Tensor( - "IS_CANCELLED", np.array([is_cancelled], dtype=np.bool_) - ), - ], - error=error, - ) - ] - - return responses diff --git a/qa/L0_backend_python/decoupled/models/decoupled_bls_async_cancel/config.pbtxt b/qa/L0_backend_python/decoupled/models/decoupled_bls_async_cancel/config.pbtxt deleted file mode 100644 index 83cc39a6f6..0000000000 --- a/qa/L0_backend_python/decoupled/models/decoupled_bls_async_cancel/config.pbtxt +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -name: "decoupled_bls_async_cancel" -backend: "python" - -input [ - { - name: "INPUT" - data_type: TYPE_INT32 - dims: [ 1 ] - }, - { - name: "MAX_SUM" - data_type: TYPE_INT32 - dims: [ 1 ] - }, - { - name: "IGNORE_CANCEL" - data_type: TYPE_BOOL - dims: [ 1 ] - } -] -output [ - { - name: "SUM" - data_type: TYPE_INT32 - dims: [ 1 ] - }, - { - name: "IS_CANCELLED" - data_type: TYPE_BOOL - dims: [ 1 ] - } -] - -instance_group [ - { - count: 1 - kind : KIND_CPU - } -] diff --git a/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel/1/model.py b/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel/1/model.py deleted file mode 100644 index dab6414ede..0000000000 --- a/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel/1/model.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import numpy as np -import triton_python_backend_utils as pb_utils - - -class TritonPythonModel: - """ - This model sends a decoupled bls inference request to 'response_sender_until_cancelled' - model, and sums up its responses. - Once the MAX_SUM is reached, the model will call the response iterator's - cancel() method to cancel the response stream. - If the IGNORE_CANCEL is set to True, the 'response_sender_until_cancelled' model will not hornor - the request cancellation and keep sending the output to the model. - The number of total responses should not reach MAX_RESPONSE_COUNT. - """ - - def execute(self, requests): - max_sum = ( - pb_utils.get_input_tensor_by_name(requests[0], "MAX_SUM").as_numpy().flat[0] - ) - input = pb_utils.get_input_tensor_by_name(requests[0], "INPUT") - ignore_cancel = pb_utils.get_input_tensor_by_name(requests[0], "IGNORE_CANCEL") - delay = pb_utils.Tensor("DELAY", np.array([50], dtype=np.int32)) - max_response_count = pb_utils.Tensor( - "MAX_RESPONSE_COUNT", np.array([20], dtype=np.int32) - ) - - infer_request = pb_utils.InferenceRequest( - model_name="response_sender_until_cancelled", - inputs=[input, max_response_count, delay, ignore_cancel], - requested_output_names=["OUTPUT"], - ) - - response_stream = infer_request.exec(decoupled=True) - - is_cancelled = False - error = None - response_sum = 0 - for infer_response in response_stream: - if infer_response.has_error(): - if infer_response.error().code() == pb_utils.TritonError.CANCELLED: - is_cancelled = True - else: - error = infer_response.error() - break - - out = pb_utils.get_output_tensor_by_name( - infer_response, "OUTPUT" - ).as_numpy()[0] - - response_sum += out - if response_sum >= max_sum: - response_stream.cancel() - - responses = [ - pb_utils.InferenceResponse( - output_tensors=[ - pb_utils.Tensor("SUM", np.array([response_sum], dtype=np.int32)), - pb_utils.Tensor( - "IS_CANCELLED", np.array([is_cancelled], dtype=np.bool_) - ), - ], - error=error, - ) - ] - - return responses diff --git a/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel/config.pbtxt b/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel/config.pbtxt deleted file mode 100644 index 0b443de6f9..0000000000 --- a/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel/config.pbtxt +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -name: "decoupled_bls_cancel" -backend: "python" - -input [ - { - name: "INPUT" - data_type: TYPE_INT32 - dims: [ 1 ] - }, - { - name: "MAX_SUM" - data_type: TYPE_INT32 - dims: [ 1 ] - }, - { - name: "IGNORE_CANCEL" - data_type: TYPE_BOOL - dims: [ 1 ] - } -] -output [ - { - name: "SUM" - data_type: TYPE_INT32 - dims: [ 1 ] - }, - { - name: "IS_CANCELLED" - data_type: TYPE_BOOL - dims: [ 1 ] - } -] - -instance_group [ - { - count: 1 - kind : KIND_CPU - } -] diff --git a/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel_after_complete/1/model.py b/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel_after_complete/1/model.py deleted file mode 100644 index 4c4202de1a..0000000000 --- a/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel_after_complete/1/model.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import asyncio - -import numpy as np -import triton_python_backend_utils as pb_utils - - -class TritonPythonModel: - """ - This model sends a decoupled bls inference request to 'response_sender_until_cancelled' - model, and sums up its responses. - Once the MAX_SUM is reached, the model will call the response iterator's - cancel() method to cancel the response stream. - If the IGNORE_CANCEL is set to True, the 'response_sender_until_cancelled' model will not hornor - the request cancellation and keep sending the output to the model. - The number of total responses should not reach MAX_RESPONSE_COUNT. - """ - - async def execute(self, requests): - max_sum = ( - pb_utils.get_input_tensor_by_name(requests[0], "MAX_SUM").as_numpy().flat[0] - ) - input = pb_utils.get_input_tensor_by_name(requests[0], "INPUT") - ignore_cancel = pb_utils.get_input_tensor_by_name(requests[0], "IGNORE_CANCEL") - delay = pb_utils.Tensor("DELAY", np.array([50], dtype=np.int32)) - max_response_count = pb_utils.Tensor( - "MAX_RESPONSE_COUNT", np.array([20], dtype=np.int32) - ) - - infer_request = pb_utils.InferenceRequest( - model_name="response_sender_until_cancelled", - inputs=[input, max_response_count, delay, ignore_cancel], - requested_output_names=["OUTPUT"], - ) - - response_stream = await infer_request.async_exec(decoupled=True) - - is_cancelled = False - error = None - response_sum = 0 - for infer_response in response_stream: - if infer_response.has_error(): - if infer_response.error().code() == pb_utils.TritonError.CANCELLED: - is_cancelled = True - else: - error = infer_response.error() - break - - out = pb_utils.get_output_tensor_by_name( - infer_response, "OUTPUT" - ).as_numpy()[0] - - response_sum += out - if response_sum >= max_sum: - response_stream.cancel() - - # test cancel after request completion. - if not error: - try: - response_stream.cancel() - except Exception as e: - error = pb_utils.TritonError( - message=str(e), - code=pb_utils.TritonError.INTERNAL, - ) - - responses = [ - pb_utils.InferenceResponse( - output_tensors=[ - pb_utils.Tensor("SUM", np.array([response_sum], dtype=np.int32)), - pb_utils.Tensor( - "IS_CANCELLED", np.array([is_cancelled], dtype=np.bool_) - ), - ], - error=error, - ) - ] - - return responses diff --git a/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel_after_complete/config.pbtxt b/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel_after_complete/config.pbtxt deleted file mode 100644 index cdbffa5419..0000000000 --- a/qa/L0_backend_python/decoupled/models/decoupled_bls_cancel_after_complete/config.pbtxt +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -name: "decoupled_bls_cancel_after_complete" -backend: "python" - -input [ - { - name: "INPUT" - data_type: TYPE_INT32 - dims: [ 1 ] - }, - { - name: "MAX_SUM" - data_type: TYPE_INT32 - dims: [ 1 ] - }, - { - name: "IGNORE_CANCEL" - data_type: TYPE_BOOL - dims: [ 1 ] - } -] -output [ - { - name: "SUM" - data_type: TYPE_INT32 - dims: [ 1 ] - }, - { - name: "IS_CANCELLED" - data_type: TYPE_BOOL - dims: [ 1 ] - } -] - -instance_group [ - { - count: 1 - kind : KIND_CPU - } -] diff --git a/qa/L0_backend_python/decoupled/test.sh b/qa/L0_backend_python/decoupled/test.sh index 622c151050..672335c892 100755 --- a/qa/L0_backend_python/decoupled/test.sh +++ b/qa/L0_backend_python/decoupled/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -53,10 +53,6 @@ cp ../../python_models/execute_cancel/model.py ./models/execute_cancel/1/ cp ../../python_models/execute_cancel/config.pbtxt ./models/execute_cancel/ echo "model_transaction_policy { decoupled: True }" >> ./models/execute_cancel/config.pbtxt -mkdir -p models/response_sender_until_cancelled/1/ -cp ../../python_models/response_sender_until_cancelled/model.py ./models/response_sender_until_cancelled/1/ -cp ../../python_models/response_sender_until_cancelled/config.pbtxt ./models/response_sender_until_cancelled/ - rm -fr python_backend git clone ${TRITON_REPO_ORGANIZATION}/python_backend -b $PYTHON_BACKEND_REPO_TAG mkdir -p models/square_int32/1/ diff --git a/qa/L0_backend_python/env/test.sh b/qa/L0_backend_python/env/test.sh index b98ba607d6..4d9de30a56 100755 --- a/qa/L0_backend_python/env/test.sh +++ b/qa/L0_backend_python/env/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -48,13 +48,13 @@ install_conda path_to_conda_pack='$$TRITON_MODEL_DIRECTORY/python_3_12_environment.tar.gz' create_conda_env "3.12" "python-3-12" conda install -c conda-forge libstdcxx-ng=14 -y -TORCH_VERSION="2.8.0" +TORCH_VERSION="2.6.0" conda install numpy=1.26.4 -y -if [[ ${TRITON_RHEL} -eq "1" ]] && grep -qE 'rhel|centos|fedora' /etc/os-release; then +if [ $TRITON_RHEL -eq 1 ]; then TORCH_VERISON="2.17.0" fi -conda install pytorch=${TORCH_VERSION} -y -PY312_VERSION_STRING="Python version is 3.12, NumPy version is 1.26.4, and PyTorch version is ${TORCH_VERSION}" +conda install torch=${TORCH_VERSION} -y +PY312_VERSION_STRING="Python version is 3.12, NumPy version is 1.26.4, and PyTorch version is ${TORCH_VERISON}" conda pack -o python3.12.tar.gz mkdir -p models/python_3_12/1/ cp ../../python_models/python_version/config.pbtxt ./models/python_3_12 @@ -122,7 +122,7 @@ fi kill_server set +e -grep "Locale is ('C', 'UTF-8')" $SERVER_LOG +grep "Locale is ('en_US', 'UTF-8')" $SERVER_LOG if [ $? -ne 0 ]; then cat $SERVER_LOG echo -e "\n***\n*** Locale UTF-8 was not found in Triton logs. \n***" @@ -182,6 +182,10 @@ aws s3 mb "${BUCKET_URL}" BUCKET_URL=${BUCKET_URL%/} BUCKET_URL_SLASH="${BUCKET_URL}/" +# Remove Python 3.7 model because it contains absolute paths and cannot be used +# with S3. +rm -rf models/python_3_7 + # Test with the bucket url as model repository aws s3 cp models/ "${BUCKET_URL_SLASH}" --recursive --include "*" @@ -201,10 +205,10 @@ fi kill_server set +e -grep "$PY312_VERSION_STRING" $SERVER_LOG +grep "$PY36_VERSION_STRING" $SERVER_LOG if [ $? -ne 0 ]; then cat $SERVER_LOG - echo -e "\n***\n*** $PY312_VERSION_STRING was not found in Triton logs. \n***" + echo -e "\n***\n*** $PY36_VERSION_STRING was not found in Triton logs. \n***" RET=1 fi set -e @@ -213,6 +217,8 @@ set -e aws s3 rm "${BUCKET_URL_SLASH}" --recursive --include "*" # Test with EXECUTION_ENV_PATH outside the model directory +sed -i "s/TRITON_MODEL_DIRECTORY\/python_3_6_environment/TRITON_MODEL_DIRECTORY\/..\/python_3_6_environment/" models/python_3_6/config.pbtxt +mv models/python_3_6/python_3_6_environment.tar.gz models sed -i "s/\$\$TRITON_MODEL_DIRECTORY\/python_3_12_environment/s3:\/\/triton-bucket-${CI_JOB_ID}\/python_3_12_environment/" models/python_3_12/config.pbtxt mv models/python_3_12/python_3_12_environment.tar.gz models @@ -232,7 +238,7 @@ fi kill_server set +e -for EXPECTED_VERSION_STRING in "$PY312_VERSION_STRING"; do +for EXPECTED_VERSION_STRING in "$PY36_VERSION_STRING" "$PY312_VERSION_STRING"; do grep "$EXPECTED_VERSION_STRING" $SERVER_LOG if [ $? -ne 0 ]; then cat $SERVER_LOG @@ -246,63 +252,6 @@ set -e aws s3 rm "${BUCKET_URL_SLASH}" --recursive --include "*" aws s3 rb "${BUCKET_URL}" -# EXECUTION_ENV_PATH path-traversal regression test. For each -# traversal vector, load a Python-backend model with archive is malicious -# and assert: (a) Triton refuses to start, (b) no file was written outside -# the extraction directory, (c) the server log contains the libarchive -# rejection. See zipslip_test.py for the archive layout. -ZIPSLIP_REPO="`pwd`/zipslip_models" -ZIPSLIP_EXECUTION_ENV_PATH='$$TRITON_MODEL_DIRECTORY/malicious_env.tar.gz' - -ZIPSLIP_CASES=( - "relative:Path contains '..'" - "absolute:Path is absolute" -) - -for case in "${ZIPSLIP_CASES[@]}"; do - mode="${case%%:*}" - expected_msg="${case#*:}" - model="zipslip_${mode}" - marker="/tmp/zipslip_${mode}_marker_$$" - SERVER_LOG="./zipslip_${mode}_server.log" - - rm -rf "${marker}" "${SERVER_LOG}" "${ZIPSLIP_REPO}" - mkdir -p "${ZIPSLIP_REPO}/${model}/1" - - cp ../../python_models/identity_fp32/config.pbtxt "${ZIPSLIP_REPO}/${model}/config.pbtxt" - cp ../../python_models/identity_fp32/model.py "${ZIPSLIP_REPO}/${model}/1/model.py" - (cd "${ZIPSLIP_REPO}/${model}" && \ - sed -i "s/^name:.*/name: \"${model}\"/" config.pbtxt && \ - echo "parameters: {key: \"EXECUTION_ENV_PATH\", value: {string_value: \"$ZIPSLIP_EXECUTION_ENV_PATH\"}}" >> config.pbtxt) - python3 ./zipslip_test.py \ - --mode "${mode}" \ - --output "${ZIPSLIP_REPO}/${model}/malicious_env.tar.gz" \ - --marker "${marker}" - - SERVER_ARGS="--model-repository=${ZIPSLIP_REPO} --log-verbose=1" - run_server - if [ "$SERVER_PID" != "0" ]; then - kill_server - echo -e "\n***\n*** Zip Slip (mode=${mode}): tritonserver started despite malicious EXECUTION_ENV_PATH archive.\n***" - RET=1 - fi - - set +e - if [ -e "${marker}" ]; then - ls -la "${marker}" - echo -e "\n***\n*** Zip Slip (mode=${mode}): marker file written outside extraction directory.\n***" - RET=1 - fi - if ! grep -q "${expected_msg}" "${SERVER_LOG}"; then - cat "${SERVER_LOG}" - echo -e "\n***\n*** Zip Slip (mode=${mode}): expected '${expected_msg}' not found in server log.\n***" - RET=1 - fi - set -e - - rm -f "${marker}" -done - if [ $RET -eq 0 ]; then echo -e "\n***\n*** Env Manager Test PASSED.\n***" else diff --git a/qa/L0_backend_python/env/zipslip_test.py b/qa/L0_backend_python/env/zipslip_test.py deleted file mode 100755 index 83336a245f..0000000000 --- a/qa/L0_backend_python/env/zipslip_test.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -""" -Build a malicious EXECUTION_ENV_PATH tarball for the Python backend path-traversal regression test. - - --mode relative -> entry '../../' (ARCHIVE_EXTRACT_SECURE_NODOTDOT) - --mode absolute -> entry '/tmp/' (ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS) -""" - -import argparse -import io -import os -import sys -import tarfile -import time - - -def build_archive(mode: str, output: str, marker: str) -> None: - if mode == "relative": - entry_name = "../../" + marker[len("/tmp/") :] - else: - entry_name = marker - - def add(tar: tarfile.TarFile, name: str, body: bytes, mode: int = 0o644): - info = tarfile.TarInfo(name=name) - info.size, info.mode, info.mtime = len(body), mode, int(time.time()) - tar.addfile(info, io.BytesIO(body)) - - os.makedirs(os.path.dirname(output) or ".", exist_ok=True) - with tarfile.open(output, "w:gz") as tar: - add(tar, entry_name, b"traversal-test\n") - # Stand-in for a conda-pack activate script so an unpatched server - # accepts the model end-to-end rather than failing on the missing - # entry-point. - add(tar, "bin/activate", b"#!/bin/bash\n", mode=0o755) - - print(f"[zipslip] wrote {output} (mode={mode}, entry={entry_name!r})") - - -def main() -> int: - ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) - ap.add_argument("--mode", required=True, choices=["relative", "absolute"]) - ap.add_argument( - "--output", required=True, help="path to write the malicious .tar.gz" - ) - ap.add_argument( - "--marker", - required=True, - help="absolute path under /tmp/ where the traversal " - "would land on a vulnerable server", - ) - args = ap.parse_args() - - try: - build_archive(args.mode, args.output, args.marker) - except ValueError as e: - print(f"error: {e}", file=sys.stderr) - return 1 - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/qa/L0_backend_python/examples/test.sh b/qa/L0_backend_python/examples/test.sh index c074b8e310..ea0fcd992c 100755 --- a/qa/L0_backend_python/examples/test.sh +++ b/qa/L0_backend_python/examples/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -69,7 +69,7 @@ pip3 install validators # Install JAX # Jax has dropped the support for Python 3.8. See https://jax.readthedocs.io/en/latest/changelog.html if [ "$TEST_JETSON" == "0" ] && [ ${PYTHON_ENV_VERSION} != "8" ]; then - pip install -U "jax[cuda12]" + pip3 install --upgrade "jax[cuda12_local]" -f https://storage.googleapis.com/jax-releases/jax_cuda_releases.html fi git clone ${TRITON_REPO_ORGANIZATION}/python_backend -b $PYTHON_BACKEND_REPO_TAG diff --git a/qa/L0_backend_python/lifecycle/lifecycle_test.py b/qa/L0_backend_python/lifecycle/lifecycle_test.py index e16c0944ff..3874ef428e 100755 --- a/qa/L0_backend_python/lifecycle/lifecycle_test.py +++ b/qa/L0_backend_python/lifecycle/lifecycle_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -255,7 +255,7 @@ def test_triton_grpc_error_error_on(self): callback=partial(callback, user_data), headers=metadata ) stream_end = False - for _ in range(number_of_requests): + for i in range(number_of_requests): input_data = np.random.randn(*shape).astype(np.float32) inputs = [ grpcclient.InferInput( @@ -348,68 +348,6 @@ def test_triton_grpc_error_cancel(self): "This should always pass as cancellation should succeed without any exception", ) - # Regression test for a segfault when a decoupled model sends an error - # response followed by a delayed FINAL flag with "triton_grpc_error" enabled. - # The error triggers stream closure, and the late FINAL callback must not - # crash by accessing a released state. - def test_triton_grpc_error_decoupled_delayed_final(self): - model_name = "decoupled_grpc_error" - shape = [2] - user_data = UserData() - metadata = {"triton_grpc_error": "true"} - - for mode in [ - "MODE_ERROR_ONLY", - "MODE_ERROR_FINAL", - "MODE_ERROR_WITH_DELAYED_FINAL", - ]: - triton_client = grpcclient.InferenceServerClient( - f"{_tritonserver_ipaddr}:8001" - ) - triton_client.start_stream( - callback=partial(callback, user_data), headers=metadata - ) - - input_data = np.random.randn(*shape).astype(np.float32) - mode_data = np.array([bytes(mode, "utf-8")], dtype=object) - inputs = [ - grpcclient.InferInput( - "IN", input_data.shape, np_to_triton_dtype(input_data.dtype) - ), - grpcclient.InferInput("MODE", mode_data.shape, "BYTES"), - ] - inputs[0].set_data_from_numpy(input_data) - inputs[1].set_data_from_numpy(mode_data) - triton_client.async_stream_infer(model_name=model_name, inputs=inputs) - - # The first response is always identical to the input data. - result = user_data._completed_requests.get() - output_data = result.as_numpy("OUT") - self.assertIsNotNone(output_data, "error: expected 'OUT'") - self.assertTrue( - np.array_equal(output_data, input_data), - "error: expected output {} to match input {}".format( - output_data, input_data - ), - ) - - # The second response is the error response. - result = user_data._completed_requests.get() - self.assertIsInstance(result, InferenceServerException) - self.assertEqual(str(result.status()), "StatusCode.INTERNAL") - - # Should not receive any subsequent responses. - with self.assertRaises(queue.Empty): - # Wait for the delayed FINAL flag (model sleeps 0.5s) plus buffer. - # Before the fix, the server would SIGSEGV here. - user_data._completed_requests.get(timeout=2) - - # Verify the server is still alive after the delayed FINAL flag. - triton_client2 = grpcclient.InferenceServerClient( - f"{_tritonserver_ipaddr}:8001" - ) - self.assertTrue(triton_client2.is_server_live()) - # Test grpc stream behavior when triton_grpc_error is set to false # and subsequent stream is NOT closed when error is reported from CORE def test_triton_grpc_error_error_off(self): diff --git a/qa/L0_backend_python/lifecycle/test.sh b/qa/L0_backend_python/lifecycle/test.sh index f53c219772..59b846f56b 100755 --- a/qa/L0_backend_python/lifecycle/test.sh +++ b/qa/L0_backend_python/lifecycle/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -60,10 +60,6 @@ cp ../../python_models/execute_grpc_error/config.pbtxt ./models/execute_grpc_err sed -i "s/^max_batch_size:.*/max_batch_size: 8/" config.pbtxt && \ echo "dynamic_batching { preferred_batch_size: [8], max_queue_delay_microseconds: 1200000 }" >> config.pbtxt) -mkdir -p models/decoupled_grpc_error/1/ -cp ../../python_models/decoupled_grpc_error/model.py ./models/decoupled_grpc_error/1/ -cp ../../python_models/decoupled_grpc_error/config.pbtxt ./models/decoupled_grpc_error/ - mkdir -p models/execute_return_error/1/ cp ../../python_models/execute_return_error/model.py ./models/execute_return_error/1/ cp ../../python_models/execute_return_error/config.pbtxt ./models/execute_return_error/ diff --git a/qa/L0_backend_python/model_control/model_control_test.py b/qa/L0_backend_python/model_control/model_control_test.py index a0ad197bb8..9ccb73df4f 100755 --- a/qa/L0_backend_python/model_control/model_control_test.py +++ b/qa/L0_backend_python/model_control/model_control_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -26,10 +26,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import base64 -import json import os -import subprocess import sys sys.path.append("../../common") @@ -86,241 +83,5 @@ def test_model_reload(self): self.assertFalse(client.is_model_ready(ensemble_model_name)) -class ModelIDValidationTest(unittest.TestCase): - """ - Test model ID validation for user-provided model names. - - Verifies that model names containing dangerous characters are properly rejected. - Uses raw HTTP requests via curl instead of the Triton client to test server-side - validation without the Triton client encoding special characters. - """ - - def setUp(self): - self._shm_leak_detector = shm_util.ShmLeakDetector() - self._client = httpclient.InferenceServerClient(f"{_tritonserver_ipaddr}:8000") - self._triton_host = _tritonserver_ipaddr - self._triton_port = 8000 - - # Check if curl is available - try: - subprocess.run(["curl", "--version"], capture_output=True, check=True) - except (subprocess.CalledProcessError, FileNotFoundError): - self.skipTest("curl command not available - required for raw HTTP testing") - - def _send_load_model_request(self, model_name): - """Send HTTP request to load model for testing input validation using curl""" - - # Create simple Triton Python model code - python_model_code = f"""import triton_python_backend_utils as pb_utils - -class TritonPythonModel: - def execute(self, requests): - print('Hello world from model {model_name}') - responses = [] - for request in requests: - # Simple identity function - input_tensor = pb_utils.get_input_tensor_by_name(request, "INPUT0") - out_tensor = pb_utils.Tensor("OUTPUT0", input_tensor.as_numpy()) - responses.append(pb_utils.InferenceResponse([out_tensor])) - return responses""" - - # Base64 encode the Python code (as required by Triton server) - python_code_b64 = base64.b64encode(python_model_code.encode("utf-8")).decode( - "ascii" - ) - - # Create simple config - config = { - "name": model_name, - "backend": "python", - "max_batch_size": 4, - "input": [{"name": "INPUT0", "data_type": "TYPE_FP32", "dims": [-1]}], - "output": [{"name": "OUTPUT0", "data_type": "TYPE_FP32", "dims": [-1]}], - } - - payload = { - "parameters": { - "config": json.dumps(config), - "file:/1/model.py": python_code_b64, - } - } - - url = f"http://{self._triton_host}:{self._triton_port}/v2/repository/models/{model_name}/load" - - # Convert payload to JSON string - payload_json = json.dumps(payload) - - try: - # Use curl to send the request - curl_cmd = [ - "curl", - "-s", - "-w", - "\n%{http_code}", # Write HTTP status code on separate line - "-X", - "POST", - "-H", - "Content-Type: application/json", - "-d", - payload_json, - url, - ] - - result = subprocess.run( - curl_cmd, capture_output=True, text=True, timeout=10 - ) - - # Parse curl output - last line is status code, rest is response body - output_lines = ( - result.stdout.strip().split("\n") if result.stdout.strip() else [] - ) - if len(output_lines) >= 2: - try: - status_code = int(output_lines[-1]) - response_text = "\n".join(output_lines[:-1]) - except ValueError: - status_code = 0 - response_text = result.stdout or result.stderr or "Invalid response" - elif len(output_lines) == 1 and output_lines[0].isdigit(): - status_code = int(output_lines[0]) - response_text = result.stderr or "No response body" - else: - status_code = 0 - response_text = result.stdout or result.stderr or "No response" - - # Return an object similar to requests.Response - class CurlResponse: - def __init__(self, status_code, text): - self.status_code = status_code - self.text = text - self.content = text.encode() - - return CurlResponse(status_code, response_text) - - except ( - subprocess.TimeoutExpired, - subprocess.CalledProcessError, - ValueError, - ) as e: - # Return a mock response for errors - class ErrorResponse: - def __init__(self, error_msg): - self.status_code = 0 - self.text = f"Error: {error_msg}" - self.content = self.text.encode() - - return ErrorResponse(str(e)) - - def test_invalid_character_model_names(self): - """Test that model names with invalid characters are properly rejected""" - - # Based on INVALID_CHARS = ";|&$`<>()[]{}\\\"'*?~#!" - invalid_model_names = [ - r"model;test", - r"model|test", - r"model&test", - r"model$test", - r"model`test`", - r"model", - r"model(test)", - # r"model[test]", # request fails to send unencoded - r"model{test}", - r"model\test", - r'model"test"', - r"model'test'", - r"model*test", - # r"model?test", # request fails to send unencoded - r"model~test", - # r"model#test", # request fails to send unencoded - r"model!test", - ] - - for invalid_name in invalid_model_names: - with self.subTest(model_name=invalid_name): - print(f"Testing invalid model name: {invalid_name}") - - response = self._send_load_model_request(invalid_name) - print( - f"Response for '{invalid_name}': Status {response.status_code}, Text: {response.text[:200]}..." - ) - - # Should not get a successful 200 response - self.assertNotEqual( - 200, - response.status_code, - f"Invalid model name '{invalid_name}' should not get 200 OK response", - ) - - # Special case for curly braces - they get stripped and cause load failures prior to the validation check - if "{" in invalid_name or "}" in invalid_name: - self.assertIn( - "failed to load", - response.text, - f"Model with curly braces '{invalid_name}' should fail to load", - ) - else: - # Normal case - should get character validation error - self.assertIn( - "Invalid stub name: contains invalid characters", - response.text, - f"invalid response for '{invalid_name}' should contain 'Invalid stub name: contains invalid characters'", - ) - - # Verify the model is not loaded/ready since it was rejected - try: - self.assertFalse( - self._client.is_model_ready(invalid_name), - f"Model '{invalid_name}' should not be ready after failed load attempt", - ) - except Exception as e: - # If checking model readiness fails, that's also acceptable since the model name is invalid - print( - f"Note: Could not check model readiness for '{invalid_name}': {e}" - ) - - def test_valid_model_names(self): - """Test that valid model names work""" - - valid_model_names = [ - "TestModel123", - "model-with-hyphens", - "model_with_underscores", - ] - - for valid_name in valid_model_names: - with self.subTest(model_name=valid_name): - print(f"Testing valid model name: {valid_name}") - - response = self._send_load_model_request(valid_name) - print( - f"Response for valid '{valid_name}': Status {response.status_code}, Text: {response.text[:100]}..." - ) - - # Valid model names should be accepted and load successfully - self.assertEqual( - 200, - response.status_code, - f"Valid model name '{valid_name}' should get 200 OK response, got {response.status_code}. Response: {response.text}", - ) - - # Should not contain validation error message - self.assertNotIn( - "Invalid stub name: contains invalid characters", - response.text, - f"Valid model name '{valid_name}' should not contain validation error message", - ) - - # Verify the model is actually loaded by checking if it's ready - try: - self.assertTrue( - self._client.is_model_ready(valid_name), - f"Model '{valid_name}' should be ready after successful load", - ) - # Clean up - unload the model after testing - self._client.unload_model(valid_name) - except Exception as e: - self.fail(f"Failed to check if model '{valid_name}' is ready: {e}") - - if __name__ == "__main__": unittest.main() diff --git a/qa/L0_backend_python/model_control/test.sh b/qa/L0_backend_python/model_control/test.sh index f841222a1b..e2c22f2685 100755 --- a/qa/L0_backend_python/model_control/test.sh +++ b/qa/L0_backend_python/model_control/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -55,24 +55,11 @@ if [ $? -ne 0 ]; then echo -e "\n***\n*** model_control_test.py FAILED. \n***" RET=1 fi - -echo -e "\n***\n*** Running model ID validation test\n***" -SUBTEST="model_id_validation" -python3 -m pytest --junitxml=model_control.${SUBTEST}.report.xml model_control_test.py::ModelIDValidationTest >> ${CLIENT_LOG} 2>&1 - -if [ $? -ne 0 ]; then - echo -e "\n***\n*** model_id_validation_test.py FAILED. \n***" - RET=1 -fi - set -e kill_server if [ $RET -eq 1 ]; then - echo -e "\n***\n*** Server logs:\n***" - cat $SERVER_LOG - echo -e "\n***\n*** Client logs:\n***" cat $CLIENT_LOG echo -e "\n***\n*** model_control_test FAILED. \n***" else diff --git a/qa/L0_backend_python/model_readiness/test.sh b/qa/L0_backend_python/model_readiness/test.sh deleted file mode 100755 index d56dd29a25..0000000000 --- a/qa/L0_backend_python/model_readiness/test.sh +++ /dev/null @@ -1,194 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -TEST_RESULT_FILE='test_results.txt' -source ../common.sh -source ../../common/util.sh - -SERVER_ARGS="--model-repository=${MODELDIR}/model_readiness/models --backend-directory=${BACKEND_DIR} --log-verbose=1" - -RET=0 -rm -fr *.log ./models - -MODEL_NAME="identity_fp32" -mkdir -p models/$MODEL_NAME/1/ -cp ../../python_models/$MODEL_NAME/model.py ./models/$MODEL_NAME/1/model.py -cp ../../python_models/$MODEL_NAME/config.pbtxt ./models/$MODEL_NAME/config.pbtxt - -# -# Test Model Readiness (TRITONBACKEND_ModelInstanceReady) -# Test with different signals to simulate various crash/exit scenarios -# 11 (SIGSEGV) - Segmentation fault / crash -# 9 (SIGKILL) - Force kill -for SIGNAL in 11 9; do - echo -e "\n***\n*** Testing model_readiness with Signal $SIGNAL\n***" - SERVER_LOG="./model_readiness_signal_${SIGNAL}_server.log" - CLIENT_LOG="./model_readiness_signal_${SIGNAL}_client.log" - - run_server - if [ "$SERVER_PID" == "0" ]; then - cat $SERVER_LOG - echo -e "\n***\n*** Failed to start $SERVER\n***" - exit 1 - fi - - set +e - - # Verify model is initially ready - echo "Checking Initial Readiness..." - python3 -m unittest test_model_readiness.TestModelReadiness.test_model_ready >> ${CLIENT_LOG} 2>&1 - if [ $? -ne 0 ]; then - echo -e "\n***\n*** Test model_readiness Failed (Signal $SIGNAL): Initial readiness check failed \n***" - RET=1 - kill_server - exit 1 - fi - - # Find the stub process PID - stub_pid=$(pgrep -f "triton_python_backend_stub") - - if [ -z "$stub_pid" ]; then - echo -e "\n***\n*** Test model_readiness Failed (Signal $SIGNAL): Could not find stub process \n***" - RET=1 - kill_server - else - echo "Found stub process: $stub_pid" - - # Kill the stub process - echo "Killing stub with signal $SIGNAL..." - kill -$SIGNAL $stub_pid - sleep 1 - - # Verify model is now NOT ready - echo "Checking Not Ready Status..." - python3 -m unittest test_model_readiness.TestModelReadiness.test_model_not_ready >> ${CLIENT_LOG} 2>&1 - if [ $? -ne 0 ]; then - echo -e "\n***\n*** Test model_readiness Failed (Signal $SIGNAL): Model reported ready after kill \n***" - RET=1 - else - # Verify correct error message in logs - # Expect 2 occurrences: HTTP and gRPC checks - error_count=$(grep -c "Model '${MODEL_NAME}' version 1 is not ready: Stub process '${MODEL_NAME}_0_0' is not healthy." $SERVER_LOG) - if [ "$error_count" -eq 2 ]; then - echo -e "\n***\n Test model_readiness Passed for Signal $SIGNAL \n***" - else - echo -e "\n***\n*** Test model_readiness Failed (Signal $SIGNAL): Expected 2 error messages, found $error_count \n***" - cat $SERVER_LOG - RET=1 - fi - fi - fi - - set -e - kill_server -done - -# -# Test User-Defined Model Readiness Function -# -echo -e "\n***\n*** Testing User-Defined is_ready() Function\n***" - -# Helper function to set up test models with different readiness behaviors based on config parameters -setup_readiness_test_model() { - local model_name=$1 - local return_value=$2 - local delay_secs=$3 - - mkdir -p ./models/$model_name/1/ - if [ "$model_name" == "is_ready_fn_coroutine_returns_true" ]; then - cp ./test_models/readiness_coroutine_model.py ./models/$model_name/1/model.py - else - cp ./test_models/readiness_model.py ./models/$model_name/1/model.py - fi - cp ./models/identity_fp32/config.pbtxt ./models/$model_name/config.pbtxt - sed -i "s/^name:.*/name: \"$model_name\"/" ./models/$model_name/config.pbtxt - cat >> ./models/$model_name/config.pbtxt << EOF -parameters: { - key: "READINESS_FN_RETURN_VALUE" - value: { string_value: "$return_value" } -} -parameters: { - key: "READINESS_FN_DELAY_SECS" - value: { string_value: "$delay_secs" } -} -EOF -} - -# Create readiness test models using shared model.py + config parameters -setup_readiness_test_model "is_ready_fn_returns_true" "true" "0.1" -setup_readiness_test_model "is_ready_fn_returns_false" "false" "0.1" -setup_readiness_test_model "is_ready_fn_raises_error" "exception" "0.1" -setup_readiness_test_model "is_ready_fn_returns_non_boolean" "non_boolean" "0.1" -setup_readiness_test_model "is_ready_fn_timeout" "true" "8" -setup_readiness_test_model "is_ready_fn_coroutine_returns_true" "coroutine" "0.1" - -# Decoupled model has a unique execute() and its own config -mkdir -p ./models/is_ready_fn_returns_true_decoupled/1/ -cp ./test_models/is_ready_fn_returns_true_decoupled/model.py \ - ./models/is_ready_fn_returns_true_decoupled/1/model.py -cp ./test_models/is_ready_fn_returns_true_decoupled/config.pbtxt \ - ./models/is_ready_fn_returns_true_decoupled/config.pbtxt - -# Start server with all models -SERVER_ARGS="--model-repository=$(pwd)/models --backend-directory=${BACKEND_DIR} --log-verbose=1 --strict-readiness=false" -SERVER_LOG="./test_user_defined_model_readiness_function_server.log" -CLIENT_LOG="./test_user_defined_model_readiness_function_client.log" - -run_server -if [ "$SERVER_PID" == "0" ]; then - cat $SERVER_LOG - echo -e "\n***\n*** Failed to start $SERVER\n***" - exit 1 -fi - -set +e - -echo "Running TestUserDefinedModelReadinessFunction..." -python3 -m unittest test_model_readiness.TestUserDefinedModelReadinessFunction -v >> ${CLIENT_LOG} 2>&1 -TEST_EXIT_CODE=$? - -if [ $TEST_EXIT_CODE -ne 0 ]; then - echo -e "\n***\n*** TestUserDefinedModelReadinessFunction FAILED\n***" - cat ${CLIENT_LOG} - RET=1 -else - echo -e "\n***\n*** TestUserDefinedModelReadinessFunction PASSED\n***" -fi - -set -e -kill_server - - -# Final result -if [ $RET -eq 0 ]; then - echo -e "\n***\n*** All Model Readiness Tests Passed\n***" -else - echo -e "\n***\n*** Model Readiness Tests FAILED\n***" -fi - -exit $RET - diff --git a/qa/L0_backend_python/model_readiness/test_model_readiness.py b/qa/L0_backend_python/model_readiness/test_model_readiness.py deleted file mode 100644 index 5330eb1c83..0000000000 --- a/qa/L0_backend_python/model_readiness/test_model_readiness.py +++ /dev/null @@ -1,518 +0,0 @@ -# Copyright 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import queue -import threading -import time -import unittest -from functools import partial - -import numpy as np -import tritonclient.grpc as grpcclient -import tritonclient.http as httpclient -from tritonclient.utils import InferenceServerException - -URL_HTTP = "localhost:8000" -URL_GRPC = "localhost:8001" -DEFAULT_RESPONSE_TIMEOUT = 60 - - -class UserData: - def __init__(self): - self._response_queue = queue.Queue() - - -def callback(user_data, result, error): - if error: - user_data._response_queue.put(error) - else: - user_data._response_queue.put(result) - - -def prepare_infer_args(input_value): - """Create InferInput and InferRequestedOutput lists for decoupled inference.""" - input_data = np.array([[input_value]], dtype=np.int32) - infer_input = [grpcclient.InferInput("IN", input_data.shape, "INT32")] - infer_input[0].set_data_from_numpy(input_data) - outputs = [grpcclient.InferRequestedOutput("OUT")] - return infer_input, outputs - - -def collect_responses(user_data, expected_responses_count): - """ - Collect up to `expected_responses_count` responses from user_data. - """ - errors = [] - responses = [] - recv_count = 0 - while recv_count < expected_responses_count: - try: - result = user_data._response_queue.get(timeout=DEFAULT_RESPONSE_TIMEOUT) - except queue.Empty: - raise Exception( - f"No response received within {DEFAULT_RESPONSE_TIMEOUT} seconds." - ) - if type(result) == InferenceServerException: - errors.append(result) - break - else: - responses.append(result.as_numpy("OUT")[0]) - recv_count = recv_count + 1 - - return errors, responses - - -def call_inference_identity_model(model_name, protocol, client): - """Send an inference request and verify the output matches the input.""" - shape = (1, 8) - input_data = np.ones(shape, dtype=np.float32) - - if protocol == "http": - inputs = [httpclient.InferInput("INPUT0", input_data.shape, "FP32")] - else: - inputs = [grpcclient.InferInput("INPUT0", input_data.shape, "FP32")] - - inputs[0].set_data_from_numpy(input_data) - result = client.infer(model_name, inputs) - output_data = result.as_numpy("OUTPUT0") - - np.testing.assert_array_almost_equal( - input_data, - output_data, - err_msg=f"Inference output mismatch for {model_name}", - ) - - -class TestModelReadiness(unittest.TestCase): - def setUp(self): - self.model_name = "identity_fp32" - self.client_http = httpclient.InferenceServerClient(url=URL_HTTP) - self.client_grpc = grpcclient.InferenceServerClient(url=URL_GRPC) - - def test_model_ready(self): - # Check HTTP - try: - is_ready = self.client_http.is_model_ready(self.model_name) - self.assertTrue( - is_ready, f"[HTTP] Model {self.model_name} should be READY but is NOT" - ) - call_inference_identity_model(self.model_name, "http", self.client_http) - except Exception as e: - self.fail(f"[HTTP] Unexpected error: {str(e)}") - - # Check gRPC - try: - is_ready = self.client_grpc.is_model_ready(self.model_name) - self.assertTrue( - is_ready, f"[gRPC] Model {self.model_name} should be READY but is NOT" - ) - call_inference_identity_model(self.model_name, "grpc", self.client_grpc) - except Exception as e: - self.fail(f"[gRPC] Unexpected error: {str(e)}") - - def test_model_not_ready(self): - # Check HTTP - try: - is_ready = self.client_http.is_model_ready(self.model_name) - self.assertFalse( - is_ready, - f"[HTTP] Model {self.model_name} should be NOT READY but is READY", - ) - except Exception as e: - self.fail(f"[HTTP] Unexpected error: {str(e)}") - - # Check gRPC - try: - is_ready = self.client_grpc.is_model_ready(self.model_name) - self.assertFalse( - is_ready, - f"[gRPC] Model {self.model_name} should be NOT READY but is READY.", - ) - except Exception as e: - self.fail(f"[gRPC] Unexpected error: {str(e)}") - - -class TestUserDefinedModelReadinessFunction(unittest.TestCase): - """ - Test user-defined is_ready() function - """ - - def setUp(self): - self.client_http = httpclient.InferenceServerClient(url=URL_HTTP) - self.client_grpc = grpcclient.InferenceServerClient(url=URL_GRPC) - - def _run_inference_decoupled(self, index, model_name, expected_responses_count): - """Send a decoupled streaming inference request and verify responses.""" - user_data = UserData() - with grpcclient.InferenceServerClient(URL_GRPC) as triton_client: - try: - inputs, outputs = prepare_infer_args(expected_responses_count) - triton_client.start_stream(callback=partial(callback, user_data)) - triton_client.async_stream_infer( - model_name=model_name, inputs=inputs, outputs=outputs - ) - - # Collect and verify responses - errors, responses = collect_responses( - user_data, expected_responses_count - ) - self.assertEqual( - len(responses), - expected_responses_count, - f"Index: {index} - Expected {expected_responses_count} responses, got {len(responses)}", - ) - self.assertEqual( - len(errors), - 0, - f"Index: {index} - Expected 0 errors, got {len(errors)}", - ) - - # Verify correctness of successful responses - for idx, output in enumerate(responses): - self.assertEqual( - output, - expected_responses_count, - msg=f"Response {idx} has incorrect value - {output}", - ) - finally: - triton_client.stop_stream() - - def test_multiple_concurrent_ready_and_infer_requests_decoupled(self): - model_name = "is_ready_fn_returns_true_decoupled" - num_requests = 16 - response_count = 8 - readiness_errors = [] - infer_errors = [] - - def readiness_wrapper(index, model_name): - try: - with grpcclient.InferenceServerClient(url=URL_GRPC) as triton_client: - is_ready = triton_client.is_model_ready(model_name) - if not is_ready: - raise AssertionError( - f"Index: {index} - GRPC client - Model {model_name} should be READY" - ) - except Exception as e: - readiness_errors.append((index, str(e))) - - def inference_wrapper(index, model_name): - try: - self._run_inference_decoupled(index, model_name, response_count) - except Exception as e: - infer_errors.append((index, str(e))) - - # Launch concurrent threads - threads = [] - for i in range(num_requests): - # Start threads with slight delay - time.sleep(0.1) - t1 = threading.Thread( - target=inference_wrapper, args=(i, model_name), name=f"infer-{i}" - ) - t2 = threading.Thread( - target=readiness_wrapper, args=(i, model_name), name=f"ready-{i}" - ) - threads.extend([t1, t2]) - t1.start() - t2.start() - - # Wait for all requests to complete - for t in threads: - t.join(timeout=120) - - for t in threads: - self.assertFalse(t.is_alive(), f"Threads are not completed: {t.name}") - - self.assertEqual( - len(readiness_errors), 0, f"Readiness errors: {readiness_errors}" - ) - self.assertEqual(len(infer_errors), 0, f"Inference errors: {infer_errors}") - - def test_is_ready_coroutine_returns_true(self): - model_name = "is_ready_fn_coroutine_returns_true" - for _ in range(5): - self.assertTrue( - self.client_http.is_model_ready(model_name), - f"HTTP - Model {model_name} (coroutine) should be READY", - ) - self.assertTrue( - self.client_grpc.is_model_ready(model_name), - f"gRPC - Model {model_name} (coroutine) should be READY", - ) - call_inference_identity_model(model_name, "http", self.client_http) - call_inference_identity_model(model_name, "grpc", self.client_grpc) - - def test_is_ready_returns_true(self): - model_name = "is_ready_fn_returns_true" - num_requests = 10 - - # Send multiple requests in sequence to ensure consistent behavior - for i in range(num_requests): - self.assertTrue( - self.client_http.is_model_ready(model_name), - f"iteration {i} - HTTP client - Model {model_name} should be READY", - ) - self.assertTrue( - self.client_grpc.is_model_ready(model_name), - f"iteration {i} - GRPC client - Model {model_name} should be READY", - ) - - # Verify inference is unaffected by readiness checks. - call_inference_identity_model(model_name, "http", self.client_http) - call_inference_identity_model(model_name, "grpc", self.client_grpc) - - def test_is_ready_returns_false(self): - model_name = "is_ready_fn_returns_false" - num_requests = 10 - - # Send multiple requests in sequence to ensure consistent behavior - for i in range(num_requests): - self.assertFalse( - self.client_http.is_model_ready(model_name), - f"iteration {i} - HTTP client - Model {model_name} should be NOT READY", - ) - self.assertFalse( - self.client_grpc.is_model_ready(model_name), - f"iteration {i} - GRPC client - Model {model_name} should be NOT READY", - ) - - # Verify inference is unaffected by readiness checks. - call_inference_identity_model(model_name, "http", self.client_http) - call_inference_identity_model(model_name, "grpc", self.client_grpc) - - def test_is_ready_raises_exception(self): - model_name = "is_ready_fn_raises_error" - num_requests = 10 - - # Send multiple requests in sequence to ensure consistent behavior - for i in range(num_requests): - self.assertFalse( - self.client_http.is_model_ready(model_name), - f"iteration {i} - HTTP client - Model {model_name} should be NOT READY (exception)", - ) - self.assertFalse( - self.client_grpc.is_model_ready(model_name), - f"iteration {i} - GRPC client - Model {model_name} should be NOT READY (exception)", - ) - - # Verify inference is unaffected by readiness checks. - call_inference_identity_model(model_name, "http", self.client_http) - call_inference_identity_model(model_name, "grpc", self.client_grpc) - - # Verify a healthy model is still ready to confirm server stability. - model_name = "is_ready_fn_returns_true" - for i in range(num_requests): - self.assertTrue( - self.client_http.is_model_ready(model_name), - f"iteration {i} - HTTP client - Model {model_name} should be READY", - ) - self.assertTrue( - self.client_grpc.is_model_ready(model_name), - f"iteration {i} - GRPC client - Model {model_name} should be READY", - ) - - # Verify inference is unaffected by readiness checks. - call_inference_identity_model(model_name, "http", self.client_http) - call_inference_identity_model(model_name, "grpc", self.client_grpc) - - def test_is_ready_returns_non_boolean(self): - model_name = "is_ready_fn_returns_non_boolean" - num_requests = 10 - - # Send multiple requests in sequence to ensure consistent behavior - for i in range(num_requests): - self.assertFalse( - self.client_http.is_model_ready(model_name), - f"iteration {i} - HTTP client - Model {model_name} should be NOT READY (wrong return type)", - ) - self.assertFalse( - self.client_grpc.is_model_ready(model_name), - f"iteration {i} - GRPC client - Model {model_name} should be NOT READY (wrong return type)", - ) - - # Verify inference is unaffected by readiness checks. - call_inference_identity_model(model_name, "http", self.client_http) - call_inference_identity_model(model_name, "grpc", self.client_grpc) - - # Verify a healthy model is still ready to confirm server stability. - model_name = "is_ready_fn_returns_true" - for i in range(num_requests): - self.assertTrue( - self.client_http.is_model_ready(model_name), - f"iteration {i} - HTTP client - Model {model_name} should be READY", - ) - self.assertTrue( - self.client_grpc.is_model_ready(model_name), - f"iteration {i} - GRPC client - Model {model_name} should be READY", - ) - - # Verify inference is unaffected by readiness checks. - call_inference_identity_model(model_name, "http", self.client_http) - call_inference_identity_model(model_name, "grpc", self.client_grpc) - - def test_is_ready_takes_long_time(self): - model_name = "is_ready_fn_timeout" - num_requests = 10 - - # Send multiple requests in sequence to ensure consistent behavior - for i in range(num_requests): - # This call should time out and return NOT_READY. - # Note: the stub will continue running is_ready() - # in the background (similar to the inference flow) - # even after the backend readiness timeout expires. - is_ready = self.client_http.is_model_ready(model_name) - self.assertFalse( - is_ready, - f"iteration {i} - HTTP client - Model {model_name} should timeout and return NOT READY", - ) - - call_inference_identity_model(model_name, "http", self.client_http) - - # This call should not create another internal IPC message. - # It must wait for the in-flight readiness check - # and return READY once that check completes. - is_ready = self.client_grpc.is_model_ready(model_name) - self.assertTrue( - is_ready, - f"iteration {i} - GRPC client - Model {model_name} should be READY", - ) - - call_inference_identity_model(model_name, "grpc", self.client_grpc) - - def test_multiple_concurrent_ready_and_infer_requests(self): - model_name = "is_ready_fn_returns_true" - ready_results = {"http": [], "grpc": []} - ready_errors = {"http": [], "grpc": []} - infer_results = {"http": [], "grpc": []} - infer_errors = {"http": [], "grpc": []} - num_requests = 16 - - def check_model_readiness(protocol, index): - try: - if protocol == "http": - with httpclient.InferenceServerClient(url=URL_HTTP) as client_http: - is_ready = client_http.is_model_ready(model_name) - ready_results["http"].append((index, is_ready)) - else: - with grpcclient.InferenceServerClient(url=URL_GRPC) as client_grpc: - is_ready = client_grpc.is_model_ready(model_name) - ready_results["grpc"].append((index, is_ready)) - except Exception as e: - ready_errors[protocol].append((index, str(e))) - - def do_inference(protocol, index): - try: - if protocol == "http": - with httpclient.InferenceServerClient(url=URL_HTTP) as client_http: - start = time.time() - call_inference_identity_model(model_name, protocol, client_http) - elapsed = time.time() - start - infer_results["http"].append((index, True, elapsed)) - else: - with grpcclient.InferenceServerClient(url=URL_GRPC) as client_grpc: - start = time.time() - call_inference_identity_model(model_name, protocol, client_grpc) - elapsed = time.time() - start - infer_results["grpc"].append((index, True, elapsed)) - except Exception as e: - infer_errors[protocol].append((index, str(e))) - - # Launch concurrent readiness and inference requests. - http_threads = [] - for i in range(num_requests): - t1 = threading.Thread(target=check_model_readiness, args=("http", i)) - t2 = threading.Thread(target=do_inference, args=("http", i)) - http_threads.extend([t1, t2]) - t1.start() - t2.start() - - # Wait for all requests to complete - for t in http_threads: - t.join(timeout=60) - - for t in http_threads: - self.assertFalse(t.is_alive(), f"HTTP threads are not completed") - - grpc_threads = [] - for i in range(num_requests): - t1 = threading.Thread(target=check_model_readiness, args=("grpc", i)) - t2 = threading.Thread(target=do_inference, args=("grpc", i)) - grpc_threads.extend([t1, t2]) - t1.start() - t2.start() - - # Wait for all requests to complete - for t in grpc_threads: - t.join(timeout=60) - - for t in grpc_threads: - self.assertFalse(t.is_alive(), f"gRPC threads are not completed") - - # Verify no errors in readiness checks - self.assertEqual( - len(ready_errors["http"]), 0, f"HTTP errors: {ready_errors['http']}" - ) - self.assertEqual( - len(ready_errors["grpc"]), 0, f"gRPC errors: {ready_errors['grpc']}" - ) - self.assertEqual( - len(ready_results["http"]), - num_requests, - f"Expected {num_requests} HTTP results", - ) - self.assertEqual( - len(ready_results["grpc"]), - num_requests, - f"Expected {num_requests} gRPC results", - ) - - # All should be True - for idx, ready in ready_results["http"]: - self.assertTrue(ready, f"HTTP check {idx} should be ready") - for idx, ready in ready_results["grpc"]: - self.assertTrue(ready, f"gRPC check {idx} should be ready") - - # Verify no errors in inference - self.assertEqual( - len(infer_errors["http"]), 0, f"Errors occurred: {infer_errors['http']}" - ) - self.assertEqual( - len(infer_errors["grpc"]), 0, f"Errors occurred: {infer_errors['grpc']}" - ) - self.assertEqual( - len(infer_results["http"]), - num_requests, - f"Expected {num_requests} HTTP inference results", - ) - self.assertEqual( - len(infer_results["grpc"]), - num_requests, - f"Expected {num_requests} gRPC inference results", - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/qa/L0_backend_python/model_readiness/test_models/is_ready_fn_returns_true_decoupled/model.py b/qa/L0_backend_python/model_readiness/test_models/is_ready_fn_returns_true_decoupled/model.py deleted file mode 100644 index e2a4dfcb0d..0000000000 --- a/qa/L0_backend_python/model_readiness/test_models/is_ready_fn_returns_true_decoupled/model.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -import time - -import numpy as np -import triton_python_backend_utils as pb_utils - - -class TritonPythonModel: - """ - Decoupled model that produces N responses based on input value. - """ - - def execute(self, requests): - for request in requests: - # Get input - number of responses to produce - in_tensor = pb_utils.get_input_tensor_by_name(request, "IN") - count = in_tensor.as_numpy().item() - - response_sender = request.get_response_sender() - out_tensor = pb_utils.Tensor("OUT", np.array([count], dtype=np.int32)) - - # Produce 'count' responses, each with 'count' as the output value - for i in range(count): - # Simulate some processing delay - time.sleep(0.1) - response = pb_utils.InferenceResponse(output_tensors=[out_tensor]) - response_sender.send(response) - - # Send final flag - response_sender.send(flags=pb_utils.TRITONSERVER_RESPONSE_COMPLETE_FINAL) - - return None - - def is_ready(self) -> bool: - # Simulate some processing delay - time.sleep(0.2) - return True diff --git a/qa/L0_backend_python/model_readiness/test_models/readiness_coroutine_model.py b/qa/L0_backend_python/model_readiness/test_models/readiness_coroutine_model.py deleted file mode 100644 index ce33afe700..0000000000 --- a/qa/L0_backend_python/model_readiness/test_models/readiness_coroutine_model.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import asyncio -import json - -import triton_python_backend_utils as pb_utils - - -class TritonPythonModel: - """ - Parameterized test model for async is_ready() testing. - - Behavior is controlled via config.pbtxt parameters: - READINESS_FN_DELAY_SECS - seconds to await before returning (e.g. "0.1") - """ - - def initialize(self, args): - model_config = json.loads(args["model_config"]) - params = model_config.get("parameters", {}) - self.readiness_delay_secs = float( - params.get("READINESS_FN_DELAY_SECS", {}).get("string_value", "0.1") - ) - - def execute(self, requests): - responses = [] - for request in requests: - input_tensor = pb_utils.get_input_tensor_by_name(request, "INPUT0") - out_tensor = pb_utils.Tensor("OUTPUT0", input_tensor.as_numpy()) - responses.append(pb_utils.InferenceResponse([out_tensor])) - return responses - - async def is_ready(self): - await asyncio.sleep(self.readiness_delay_secs) - return True diff --git a/qa/L0_backend_python/model_readiness/test_models/readiness_model.py b/qa/L0_backend_python/model_readiness/test_models/readiness_model.py deleted file mode 100644 index 444bd9e983..0000000000 --- a/qa/L0_backend_python/model_readiness/test_models/readiness_model.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import json -import time - -import triton_python_backend_utils as pb_utils - - -class TritonPythonModel: - """ - Parameterized test model for user-defined is_ready() testing. - - Behavior is controlled via config.pbtxt parameters: - READINESS_FN_RETURN_VALUE - "true", "false", "exception", or "non_boolean" - READINESS_FN_DELAY_SECS - seconds to sleep before returning (e.g. "0.1") - """ - - def initialize(self, args): - model_config = json.loads(args["model_config"]) - params = model_config.get("parameters", {}) - self.readiness_return_value = params.get("READINESS_FN_RETURN_VALUE", {}).get( - "string_value", "true" - ) - self.readiness_delay_secs = float( - params.get("READINESS_FN_DELAY_SECS", {}).get("string_value", "0") - ) - - def execute(self, requests): - responses = [] - for request in requests: - input_tensor = pb_utils.get_input_tensor_by_name(request, "INPUT0") - out_tensor = pb_utils.Tensor("OUTPUT0", input_tensor.as_numpy()) - responses.append(pb_utils.InferenceResponse([out_tensor])) - return responses - - def is_ready(self): - if self.readiness_delay_secs > 0: - time.sleep(self.readiness_delay_secs) - - if self.readiness_return_value == "true": - return True - elif self.readiness_return_value == "false": - return False - elif self.readiness_return_value == "exception": - raise RuntimeError("Internal check failed – model is not ready") - elif self.readiness_return_value == "non_boolean": - return "ready" - return True diff --git a/qa/L0_backend_python/python_test.py b/qa/L0_backend_python/python_test.py index 135aba3060..c2512600e2 100755 --- a/qa/L0_backend_python/python_test.py +++ b/qa/L0_backend_python/python_test.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -33,7 +33,6 @@ import os import unittest -import ml_dtypes import numpy as np import requests as httpreq import shm_util @@ -373,7 +372,9 @@ def test_bf16(self): with httpclient.InferenceServerClient( f"{_tritonserver_ipaddr}:8000" ) as client: - np_input = np.ones(shape, dtype=ml_dtypes.bfloat16) + # NOTE: Client will truncate FP32 to BF16 internally + # since numpy has no built-in BF16 representation. + np_input = np.ones(shape, dtype=np.float32) inputs = [ httpclient.InferInput( "INPUT0", np_input.shape, "BF16" @@ -389,8 +390,10 @@ def test_bf16(self): np_output = result.as_numpy("OUTPUT0") self.assertIsNotNone(np_output) - self.assertEqual(np_output.dtype, np_input.dtype) - self.assertTrue(np.array_equal(np_output, np_input)) + # BF16 tensors are held in FP32 when converted to numpy due to + # lack of native BF16 support in numpy, so verify that. + self.assertEqual(np_output.dtype, np.float32) + self.assertTrue(np.allclose(np_output, np_input)) def test_infer_pytorch(self): # FIXME: This model requires torch. Because windows tests are not run in a docker diff --git a/qa/L0_backend_python/setup_python_enviroment.sh b/qa/L0_backend_python/setup_python_enviroment.sh index ce6be322ba..c3d0a0aed7 100755 --- a/qa/L0_backend_python/setup_python_enviroment.sh +++ b/qa/L0_backend_python/setup_python_enviroment.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -56,10 +56,10 @@ conda update -n base -c defaults conda -y # been setup correctly. if [ ${PYTHON_ENV_VERSION} = "11" ]; then create_conda_env "3.11" "python-3-11" - conda install pytorch=2.8.0 -y + conda install torch=2.6.0 -y conda install -c conda-forge libstdcxx-ng=14 -y conda install numpy=1.23.5 -y - EXPECTED_VERSION_STRING="Python version is 3.11, NumPy version is 1.23.5, and PyTorch version is 2.8.0" + EXPECTED_VERSION_STRING="Python version is 3.11, NumPy version is 1.23.5, and PyTorch version is 2.6.0" create_python_backend_stub conda-pack -o python3.11.tar.gz path_to_conda_pack="$PWD/python-3-11" @@ -99,7 +99,6 @@ echo "python environment 3.${PYTHON_ENV_VERSION}" # copy the stub out to /opt/tritonserver/backends/python/triton_python_backend_stub cp python_backend/builddir/triton_python_backend_stub /opt/tritonserver/backends/python/triton_python_backend_stub # Set up environment and stub for each test -apt-get update -qq && apt-get install -y software-properties-common add-apt-repository ppa:deadsnakes/ppa -y apt-get update && apt-get -y install \ "python3.${PYTHON_ENV_VERSION}-dev" \ @@ -109,12 +108,11 @@ rm -f /usr/bin/python3 && \ ln -s "/usr/bin/python3.${PYTHON_ENV_VERSION}" /usr/bin/python3 pip3 install --upgrade requests numpy virtualenv protobuf find /opt/tritonserver/qa/pkgs/ -maxdepth 1 -type f -name \ - "tritonclient-*-py3-none-any.whl" | xargs printf -- '%s[all]' | \ + "tritonclient-*linux*.whl" | xargs printf -- '%s[all]' | \ xargs pip3 install --upgrade # Build triton-shm-monitor for the test cd python_backend && rm -rf install build && mkdir build && cd build && \ - export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DCMAKE_INSTALL_PREFIX:PATH=$PWD/install \ -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ -DTRITON_COMMON_REPO_TAG:STRING=${TRITON_COMMON_REPO_TAG} \ diff --git a/qa/L0_backend_python/test.sh b/qa/L0_backend_python/test.sh index bc739a6219..b2e6b8f034 100755 --- a/qa/L0_backend_python/test.sh +++ b/qa/L0_backend_python/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2020-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -65,6 +65,9 @@ export PYTHON_ENV_VERSION=${PYTHON_ENV_VERSION:="12"} export PYTHON_BACKEND_REPO_TAG=$PYTHON_BACKEND_REPO_TAG BASE_SERVER_ARGS="--model-repository=${MODELDIR}/models --backend-directory=${BACKEND_DIR} --log-verbose=1" +# Set the default byte size to 5MBs to avoid going out of shared memory. The +# environment that this job runs on has only 1GB of shared-memory available. +SERVER_ARGS="$BASE_SERVER_ARGS --backend-config=python,shm-default-byte-size=5242880" CLIENT_PY=./python_test.py CLIENT_LOG="./client.log" @@ -174,9 +177,6 @@ fi pip3 install pytest requests virtualenv -# Set the default byte size to 5MBs to avoid going out of shared memory. The -# environment that this job runs on has only 1GB of shared-memory available. -SERVER_ARGS="$BASE_SERVER_ARGS --allow-client-shm=true --backend-config=python,shm-default-byte-size=5242880" prev_num_pages=`get_shm_pages` run_server if [ "$SERVER_PID" == "0" ]; then @@ -205,7 +205,6 @@ and shared memory pages after starting triton equals to $current_num_pages \n*** RET=1 fi -SERVER_ARGS="$BASE_SERVER_ARGS --backend-config=python,shm-default-byte-size=5242880" prev_num_pages=`get_shm_pages` # Triton non-graceful exit run_server @@ -408,65 +407,6 @@ and shared memory pages after starting triton equals to $current_num_pages \n*** exit 1 fi - -# Test model with non-existent model file -# The model.py file is intentionally not created to trigger the failure. -rm -fr ./models -mkdir -p models/non_existent_model/1 -cp ../python_models/identity_fp32/config.pbtxt ./models/non_existent_model/config.pbtxt -(cd models/non_existent_model && \ - sed -i "s/^name:.*/name: \"non_existent_model\"/" config.pbtxt) - -ERROR_MESSAGE_1="Failed to preinitialize Python stub: Python model file not found in '`pwd`/models/non_existent_model/1/model.py'" -ERROR_MESSAGE_2="failed to load 'non_existent_model'" - -for test_mode in "default" "auto_config_disabled"; do - if [ "$test_mode" == "default" ]; then - SERVER_LOG_SUFFIX="" - SERVER_ARGS=$BASE_SERVER_ARGS - else - SERVER_LOG_SUFFIX="_auto_config_disabled" - SERVER_ARGS="$BASE_SERVER_ARGS --disable-auto-complete-config" - fi - - SERVER_LOG="./non_existent_model_server${SERVER_LOG_SUFFIX}.log" - CLIENT_LOG="./non_existent_model_client${SERVER_LOG_SUFFIX}.log" - - prev_num_pages=`get_shm_pages` - run_server - if [ "$SERVER_PID" != "0" ]; then - echo -e "*** FAILED: unexpected success starting $SERVER" >> $CLIENT_LOG - RET=1 - kill_server - else - if grep -q "$ERROR_MESSAGE_1" $SERVER_LOG; then - echo -e "Found \"$ERROR_MESSAGE_1\"" >> $CLIENT_LOG - else - echo -e "Not found \"$ERROR_MESSAGE_1\" in $SERVER_LOG" >> $CLIENT_LOG - cat $SERVER_LOG >> $CLIENT_LOG - RET=1 - fi - - if grep -q "$ERROR_MESSAGE_2" $SERVER_LOG; then - echo -e "Found \"$ERROR_MESSAGE_2\"" >> $CLIENT_LOG - else - echo -e "Not found \"$ERROR_MESSAGE_2\" in $SERVER_LOG" >> $CLIENT_LOG - cat $SERVER_LOG >> $CLIENT_LOG - RET=1 - fi - fi - - current_num_pages=`get_shm_pages` - if [ $current_num_pages -ne $prev_num_pages ]; then - cat $SERVER_LOG - ls /dev/shm - echo -e "\n***\n*** Test Failed. Shared memory pages were not cleaned properly. -Shared memory pages before starting triton equals to $prev_num_pages -and shared memory pages after starting triton equals to $current_num_pages \n***" - exit 1 - fi -done - # Disable env test for Jetson since cloud storage repos are not supported # Disable ensemble, io and bls tests for Jetson since GPU Tensors are not supported # Disable variants test for Jetson since already built without GPU Tensor support @@ -523,7 +463,7 @@ SUBTESTS="lifecycle argument_validation logging custom_metrics parameters" # [DLIS-6123] Disable examples test for Windows since it requires updates to the example clients if [[ ${TEST_WINDOWS} == 0 ]]; then # TODO: Reimplement restart on decoupled data pipeline and enable restart. - SUBTESTS+=" model_control examples request_rescheduling model_readiness" + SUBTESTS+=" model_control examples request_rescheduling" fi for TEST in ${SUBTESTS}; do # Run each subtest in a separate virtual environment to avoid conflicts diff --git a/qa/L0_backend_python/variants/test.sh b/qa/L0_backend_python/variants/test.sh index 3e3677cbe4..86cc793a94 100755 --- a/qa/L0_backend_python/variants/test.sh +++ b/qa/L0_backend_python/variants/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -34,7 +34,6 @@ rm -rf python_backend git clone ${TRITON_REPO_ORGANIZATION}/python_backend -b $PYTHON_BACKEND_REPO_TAG (cd python_backend/ && mkdir builddir && cd builddir && \ - export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DTRITON_ENABLE_GPU=OFF -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} -DTRITON_BACKEND_REPO_TAG=$TRITON_BACKEND_REPO_TAG -DTRITON_COMMON_REPO_TAG=$TRITON_COMMON_REPO_TAG -DTRITON_CORE_REPO_TAG=$TRITON_CORE_REPO_TAG ../ && \ make -j18 install) diff --git a/qa/L0_backend_tutorial/test.sh b/qa/L0_backend_tutorial/test.sh index 60c98c18f8..3e59503b8c 100755 --- a/qa/L0_backend_tutorial/test.sh +++ b/qa/L0_backend_tutorial/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -48,7 +48,7 @@ apt update -q=2 \ && . /etc/os-release \ && echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $UBUNTU_CODENAME main" | tee /etc/apt/sources.list.d/kitware.list >/dev/null \ && apt-get update -q=2 \ - && apt-get install -y --no-install-recommends cmake=4.0.3* cmake-data=4.0.3* \ + && apt-get install -y --no-install-recommends cmake=3.28.3* cmake-data=3.28.3* \ rapidjson-dev cmake --version @@ -62,7 +62,6 @@ git clone --single-branch --depth=1 -b $TRITON_BACKEND_REPO_TAG \ (cd backend/examples/backends/minimal && mkdir build && cd build && - export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DCMAKE_INSTALL_PREFIX:PATH=`pwd`/install \ -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ -DTRITON_BACKEND_REPO_TAG=${TRITON_BACKEND_REPO_TAG} \ @@ -141,7 +140,6 @@ rm -fr /opt/tritonserver/backends/minimal (cd backend/examples/backends/recommended && mkdir build && cd build && - export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DCMAKE_INSTALL_PREFIX:PATH=`pwd`/install \ -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ -DTRITON_BACKEND_REPO_TAG=${TRITON_BACKEND_REPO_TAG} \ diff --git a/qa/L0_batch_custom/test.sh b/qa/L0_batch_custom/test.sh index 5ed0f3a91b..13d54c25d8 100755 --- a/qa/L0_batch_custom/test.sh +++ b/qa/L0_batch_custom/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -54,8 +54,6 @@ TEST_RESULT_FILE='test_results.txt' TRITON_REPO_ORGANIZATION=${TRITON_REPO_ORGANIZATION:="http://github.com/triton-inference-server"} TRITON_BACKEND_REPO_TAG=${TRITON_BACKEND_REPO_TAG:="main"} TRITON_CORE_REPO_TAG=${TRITON_CORE_REPO_TAG:="main"} -TRITON_COMMON_REPO_TAG=${TRITON_COMMON_REPO_TAG:="main"} - source ../common/util.sh RET=0 @@ -68,7 +66,7 @@ apt update -q=2 \ && . /etc/os-release \ && echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $UBUNTU_CODENAME main" | tee /etc/apt/sources.list.d/kitware.list >/dev/null \ && apt-get update -q=2 \ - && apt-get install -y --no-install-recommends cmake=4.0.3* cmake-data=4.0.3* rapidjson-dev + && apt-get install -y --no-install-recommends cmake=3.28.3* cmake-data=3.28.3* rapidjson-dev cmake --version # Set up repository @@ -88,23 +86,17 @@ git clone --single-branch --depth=1 -b $TRITON_BACKEND_REPO_TAG \ (cd backend/examples/batching_strategies/volume_batching && mkdir build && cd build && - export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DCMAKE_INSTALL_PREFIX:PATH=`pwd`/install \ - -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ - -DTRITON_BACKEND_REPO_TAG=${TRITON_BACKEND_REPO_TAG} \ - -DTRITON_CORE_REPO_TAG=${TRITON_CORE_REPO_TAG} \ - -DTRITON_COMMON_REPO_TAG=${TRITON_COMMON_REPO_TAG} .. && + -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ + -DTRITON_CORE_REPO_TAG=$TRITON_CORE_REPO_TAG .. && make -j4 install) (cd backend/examples/batching_strategies/single_batching && mkdir build && cd build && - export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DCMAKE_INSTALL_PREFIX:PATH=`pwd`/install \ -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ - -DTRITON_BACKEND_REPO_TAG=${TRITON_BACKEND_REPO_TAG} \ - -DTRITON_CORE_REPO_TAG=${TRITON_CORE_REPO_TAG} \ - -DTRITON_COMMON_REPO_TAG=${TRITON_COMMON_REPO_TAG} .. && + -DTRITON_CORE_REPO_TAG=$TRITON_CORE_REPO_TAG .. && make -j4 install) cp -r backend/examples/batching_strategies/volume_batching/build/libtriton_volumebatching.so models @@ -172,10 +164,8 @@ sed -i "s/${OLD_STRING}/${NEW_STRING}/g" ${FILE_PATH} (cd backend/examples/batching_strategies/volume_batching && cd build && cmake -DCMAKE_INSTALL_PREFIX:PATH=`pwd`/install \ - -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ - -DTRITON_BACKEND_REPO_TAG=${TRITON_BACKEND_REPO_TAG} \ - -DTRITON_CORE_REPO_TAG=${TRITON_CORE_REPO_TAG} \ - -DTRITON_COMMON_REPO_TAG=${TRITON_COMMON_REPO_TAG} .. && + -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ + -DTRITON_CORE_REPO_TAG=$TRITON_CORE_REPO_TAG .. && make -j4 install) cp -r backend/examples/batching_strategies/volume_batching/build/libtriton_volumebatching.so models/${MODEL_NAME}/libtriton_volumebatching.so diff --git a/qa/L0_batcher/batcher_test.py b/qa/L0_batcher/batcher_test.py index 6a133238ef..780eb1e68f 100755 --- a/qa/L0_batcher/batcher_test.py +++ b/qa/L0_batcher/batcher_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -74,7 +74,6 @@ _ragged_batch_supported_trials.append("libtorch") _max_queue_delay_ms = 10000 -_max_expected_response_ms = 3000 _deferred_exceptions_lock = threading.Lock() _deferred_exceptions = [] @@ -312,13 +311,13 @@ def test_static_batch_preferred(self): self.check_response( trial, 2, - (_max_expected_response_ms, None), + (3000, None), precreated_shm_regions=precreated_shm_regions, ) self.check_response( trial, 6, - (_max_expected_response_ms, None), + (3000, None), precreated_shm_regions=precreated_shm_regions, ) self.check_deferred_exception() @@ -393,7 +392,7 @@ def test_static_batch_gt_max_preferred(self): self.check_response( trial, 7, - (_max_expected_response_ms, None), + (3000, None), precreated_shm_regions=precreated_shm_regions, ) self.check_deferred_exception() @@ -476,7 +475,7 @@ def test_multi_batch_different_shape(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "input_size": 16, "shm_region_names": shm0_region_names, @@ -601,7 +600,7 @@ def test_multi_batch_not_preferred_different_shape(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm0_region_names, "precreated_shm_regions": precreated_shm0_regions, @@ -611,7 +610,7 @@ def test_multi_batch_not_preferred_different_shape(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 3, (_max_expected_response_ms * 2, None)), + args=(trial, 3, (6000, None)), kwargs={ "shm_region_names": shm1_region_names, "precreated_shm_regions": precreated_shm1_regions, @@ -679,7 +678,7 @@ def test_multi_batch_preferred_different_shape(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm0_region_names, "precreated_shm_regions": precreated_shm0_regions, @@ -689,7 +688,7 @@ def test_multi_batch_preferred_different_shape(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 3, (_max_expected_response_ms * 2, None)), + args=(trial, 3, (6000, None)), kwargs={ "shm_region_names": shm1_region_names, "precreated_shm_regions": precreated_shm1_regions, @@ -699,7 +698,7 @@ def test_multi_batch_preferred_different_shape(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "input_size": 8, "shm_region_names": shm2_region_names, @@ -710,7 +709,7 @@ def test_multi_batch_preferred_different_shape(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 5, (_max_expected_response_ms * 2, None)), + args=(trial, 5, (6000, None)), kwargs={ "input_size": 8, "shm_region_names": shm3_region_names, @@ -757,7 +756,7 @@ def test_multi_batch_gt_max_preferred(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 3, (_max_expected_response_ms, None)), + args=(trial, 3, (3000, None)), kwargs={ "shm_region_names": shm0_region_names, "precreated_shm_regions": precreated_shm0_regions, @@ -767,7 +766,7 @@ def test_multi_batch_gt_max_preferred(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 7, (_max_expected_response_ms, None)), + args=(trial, 7, (3000, None)), kwargs={ "shm_region_names": shm1_region_names, "precreated_shm_regions": precreated_shm1_regions, @@ -814,7 +813,7 @@ def test_multi_batch_sum_gt_max_preferred(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 3, (_max_expected_response_ms, None)), + args=(trial, 3, (3000, None)), kwargs={ "shm_region_names": shm0_region_names, "precreated_shm_regions": precreated_shm0_regions, @@ -871,7 +870,7 @@ def test_multi_same_output0(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms, None)), + args=(trial, 1, (3000, None)), kwargs={ "requested_outputs": ("OUTPUT0",), "shm_region_names": shm0_region_names, @@ -882,7 +881,7 @@ def test_multi_same_output0(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms, None)), + args=(trial, 1, (3000, None)), kwargs={ "requested_outputs": ("OUTPUT0",), "shm_region_names": shm1_region_names, @@ -925,7 +924,7 @@ def test_multi_same_output1(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms, None)), + args=(trial, 1, (3000, None)), kwargs={ "requested_outputs": ("OUTPUT1",), "shm_region_names": shm0_region_names, @@ -936,7 +935,7 @@ def test_multi_same_output1(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms, None)), + args=(trial, 1, (3000, None)), kwargs={ "requested_outputs": ("OUTPUT1",), "shm_region_names": shm1_region_names, @@ -980,7 +979,7 @@ def test_multi_different_outputs(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "requested_outputs": ("OUTPUT0",), "shm_region_names": shm0_region_names, @@ -991,7 +990,7 @@ def test_multi_different_outputs(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "requested_outputs": ("OUTPUT1",), "shm_region_names": shm1_region_names, @@ -1032,7 +1031,7 @@ def test_multi_different_output_order(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "requested_outputs": ("OUTPUT0", "OUTPUT1"), "shm_region_names": shm0_region_names, @@ -1042,7 +1041,7 @@ def test_multi_different_output_order(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "requested_outputs": ("OUTPUT1", "OUTPUT0"), "shm_region_names": shm1_region_names, @@ -1091,7 +1090,7 @@ def test_multi_batch_delayed_sum_gt_max_preferred(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 3, (_max_expected_response_ms * 2, None)), + args=(trial, 3, (6000, None)), kwargs={ "shm_region_names": shm0_region_names, "precreated_shm_regions": precreated_shm0_regions, @@ -1188,7 +1187,7 @@ def test_multi_batch_delayed_use_max_batch(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm2_region_names, "precreated_shm_regions": precreated_shm2_regions, @@ -1246,7 +1245,7 @@ def test_multi_batch_delayed_preferred_different_shape(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms, None)), + args=(trial, 1, (3000, None)), kwargs={ "shm_region_names": shm0_region_names, "precreated_shm_regions": precreated_shm0_regions, @@ -1256,7 +1255,7 @@ def test_multi_batch_delayed_preferred_different_shape(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 3, (_max_expected_response_ms, None)), + args=(trial, 3, (3000, None)), kwargs={ "shm_region_names": shm1_region_names, "precreated_shm_regions": precreated_shm1_regions, @@ -1266,7 +1265,7 @@ def test_multi_batch_delayed_preferred_different_shape(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms, None)), + args=(trial, 1, (3000, None)), kwargs={ "input_size": 8, "shm_region_names": shm2_region_names, @@ -1277,7 +1276,7 @@ def test_multi_batch_delayed_preferred_different_shape(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 5, (_max_expected_response_ms, None)), + args=(trial, 5, (3000, None)), kwargs={ "input_size": 8, "shm_region_names": shm3_region_names, @@ -1339,7 +1338,7 @@ def test_multi_batch_use_biggest_preferred(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm0_region_names, "precreated_shm_regions": precreated_shm0_regions, @@ -1349,7 +1348,7 @@ def test_multi_batch_use_biggest_preferred(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm1_region_names, "precreated_shm_regions": precreated_shm1_regions, @@ -1359,7 +1358,7 @@ def test_multi_batch_use_biggest_preferred(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm2_region_names, "precreated_shm_regions": precreated_shm2_regions, @@ -1369,7 +1368,7 @@ def test_multi_batch_use_biggest_preferred(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm3_region_names, "precreated_shm_regions": precreated_shm3_regions, @@ -1379,7 +1378,7 @@ def test_multi_batch_use_biggest_preferred(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm4_region_names, "precreated_shm_regions": precreated_shm4_regions, @@ -1389,7 +1388,7 @@ def test_multi_batch_use_biggest_preferred(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm5_region_names, "precreated_shm_regions": precreated_shm5_regions, @@ -1440,7 +1439,7 @@ def test_multi_batch_use_best_preferred(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm0_region_names, "precreated_shm_regions": precreated_shm0_regions, @@ -1450,7 +1449,7 @@ def test_multi_batch_use_best_preferred(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm1_region_names, "precreated_shm_regions": precreated_shm1_regions, @@ -1561,7 +1560,7 @@ def test_preferred_batch_only_aligned(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm0_region_names, "precreated_shm_regions": precreated_shm0_regions, @@ -1571,7 +1570,7 @@ def test_preferred_batch_only_aligned(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm1_region_names, "precreated_shm_regions": precreated_shm1_regions, @@ -1581,7 +1580,7 @@ def test_preferred_batch_only_aligned(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm2_region_names, "precreated_shm_regions": precreated_shm2_regions, @@ -1591,7 +1590,7 @@ def test_preferred_batch_only_aligned(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm3_region_names, "precreated_shm_regions": precreated_shm3_regions, @@ -1646,7 +1645,7 @@ def test_preferred_batch_only_unaligned(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm0_region_names, "precreated_shm_regions": precreated_shm0_regions, @@ -1656,7 +1655,7 @@ def test_preferred_batch_only_unaligned(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm1_region_names, "precreated_shm_regions": precreated_shm1_regions, @@ -1666,7 +1665,7 @@ def test_preferred_batch_only_unaligned(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm2_region_names, "precreated_shm_regions": precreated_shm2_regions, @@ -1676,7 +1675,7 @@ def test_preferred_batch_only_unaligned(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm3_region_names, "precreated_shm_regions": precreated_shm3_regions, @@ -1686,7 +1685,7 @@ def test_preferred_batch_only_unaligned(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm4_region_names, "precreated_shm_regions": precreated_shm4_regions, @@ -1747,7 +1746,7 @@ def test_preferred_batch_only_use_biggest_preferred(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm0_region_names, "precreated_shm_regions": precreated_shm0_regions, @@ -1757,7 +1756,7 @@ def test_preferred_batch_only_use_biggest_preferred(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm1_region_names, "precreated_shm_regions": precreated_shm1_regions, @@ -1767,7 +1766,7 @@ def test_preferred_batch_only_use_biggest_preferred(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm2_region_names, "precreated_shm_regions": precreated_shm2_regions, @@ -1777,7 +1776,7 @@ def test_preferred_batch_only_use_biggest_preferred(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm3_region_names, "precreated_shm_regions": precreated_shm3_regions, @@ -1787,7 +1786,7 @@ def test_preferred_batch_only_use_biggest_preferred(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm4_region_names, "precreated_shm_regions": precreated_shm4_regions, @@ -1797,7 +1796,7 @@ def test_preferred_batch_only_use_biggest_preferred(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm5_region_names, "precreated_shm_regions": precreated_shm5_regions, @@ -1807,7 +1806,7 @@ def test_preferred_batch_only_use_biggest_preferred(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm6_region_names, "precreated_shm_regions": precreated_shm6_regions, @@ -1855,7 +1854,7 @@ def test_preferred_batch_only_use_no_preferred_size(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm0_region_names, "precreated_shm_regions": precreated_shm0_regions, @@ -1865,7 +1864,7 @@ def test_preferred_batch_only_use_no_preferred_size(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm1_region_names, "precreated_shm_regions": precreated_shm1_regions, @@ -1875,7 +1874,7 @@ def test_preferred_batch_only_use_no_preferred_size(self): threads.append( threading.Thread( target=self.check_response, - args=(trial, 1, (_max_expected_response_ms * 2, None)), + args=(trial, 1, (6000, None)), kwargs={ "shm_region_names": shm2_region_names, "precreated_shm_regions": precreated_shm2_regions, diff --git a/qa/L0_batcher/test.sh b/qa/L0_batcher/test.sh index a17594f8f9..136fbe586d 100755 --- a/qa/L0_batcher/test.sh +++ b/qa/L0_batcher/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -42,6 +42,7 @@ fi # can fail when the requests are distributed to multiple devices. export CUDA_VISIBLE_DEVICES=0 +CLIENT_LOG="./client.log" BATCHER_TEST=batcher_test.py VERIFY_TIMESTAMPS=verify_timestamps.py TEST_RESULT_FILE='test_results.txt' @@ -103,9 +104,6 @@ else fi SERVER_ARGS_EXTRA="--backend-directory=${BACKEND_DIR}" -if [ "$TEST_SYSTEM_SHARED_MEMORY" -eq 1 ] || [ "$TEST_CUDA_SHARED_MEMORY" -eq 1 ]; then - SERVER_ARGS_EXTRA="${SERVER_ARGS_EXTRA} --allow-client-shm=true" -fi source ../common/util.sh RET=0 @@ -267,77 +265,17 @@ if [[ $BACKENDS == *"libtorch"* ]]; then dynamic_batching { preferred_batch_size: [ 2, 6 ], max_queue_delay_microseconds: 10000000 }" >> config.pbtxt) fi -warmup_cuda_cache() { - local backend=$1 - local batch_size=$2 - local model="${backend}_float32_float32_float32" - local n=$((16 * batch_size)) - local input0_data=$(printf '1,%.0s' $(seq 1 $n) | sed 's/,$//') - local input1_data=$(printf '2,%.0s' $(seq 1 $n) | sed 's/,$//') - - SERVER_ARGS="--model-repository=$MODELDIR/models --model-control-mode=explicit --load-model=$model" - SERVER_LOG="./warmup_cuda_cache.server.log" - - run_server - - if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 - fi - - set +e - - curl -X POST "http://localhost:8000/v2/models/${model}/versions/1/infer" \ - -H "Content-Type: application/json" \ - -d '{ - "inputs": [ - { - "name": "INPUT0", - "datatype": "FP32", - "shape": ['"$batch_size"', 16], - "data": ['"$input0_data"'] - }, - { - "name": "INPUT1", - "datatype": "FP32", - "shape": ['"$batch_size"', 16], - "data": ['"$input1_data"'] - } - ], - "outputs": [ - { "name": "OUTPUT0" }, - { "name": "OUTPUT1" } - ] - }' - - EXIT_CODE=$? - if [ $EXIT_CODE -ne 0 ]; then - echo -e "\n***\n*** Test Failed\n***" - echo -e "\n***\n*** exit code: $EXIT_CODE\n***" - RET=1 - fi - set -e - - kill_server -} - -# [TRI-830] Send a simple request to warmup CUDA_CACHE for GB300 before testing. -if nvidia-smi --query-gpu=name --format=csv,noheader | grep -qiF 'GB300'; then - warmup_cuda_cache onnx 2 -fi - # Need to launch the server for each test so that the model status is # reset (which is used to make sure the correctly batch size was used # for execution). Test everything with fixed-tensor-size models and # variable-tensor-size models. + for model_type in FIXED VARIABLE; do export BATCHER_TYPE=$model_type MODEL_PATH=models && [[ "$model_type" == "VARIABLE" ]] && MODEL_PATH=var_models for i in $NO_DELAY_TESTS ; do SERVER_ARGS="--model-repository=$MODELDIR/$MODEL_PATH ${SERVER_ARGS_EXTRA}" SERVER_LOG="./$i.$model_type.server.log" - CLIENT_LOG="./$i.$model_type.client.log" if [ "$TEST_VALGRIND" -eq 1 ]; then LEAKCHECK_LOG="./$i.$model_type.valgrind.log" diff --git a/qa/L0_buffer_attributes/test.sh b/qa/L0_buffer_attributes/test.sh index d07c63e11e..7e2f35d837 100755 --- a/qa/L0_buffer_attributes/test.sh +++ b/qa/L0_buffer_attributes/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -52,7 +52,7 @@ export CUDA_VISIBLE_DEVICES=0 rm -fr *.log SERVER=/opt/tritonserver/bin/tritonserver -SERVER_ARGS="--model-repository=`pwd`/models --allow-client-shm=true" +SERVER_ARGS="--model-repository=`pwd`/models" SERVER_LOG="./inference_server.log" run_server if [ "$SERVER_PID" == "0" ]; then diff --git a/qa/L0_client_build_variants/test.sh b/qa/L0_client_build_variants/test.sh index cafc55acc7..a64d698526 100755 --- a/qa/L0_client_build_variants/test.sh +++ b/qa/L0_client_build_variants/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -38,7 +38,7 @@ apt update -q=2 \ && . /etc/os-release \ && echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $UBUNTU_CODENAME main" | tee /etc/apt/sources.list.d/kitware.list >/dev/null \ && apt-get update -q=2 \ - && apt-get install -y --no-install-recommends cmake=4.0.3* cmake-data=4.0.3* + && apt-get install -y --no-install-recommends cmake=3.28.3* cmake-data=3.28.3* cmake --version @@ -50,15 +50,8 @@ mkdir -p /workspace/build # Build without GPU support # TRITON_REPO_ORGANIZATION=${TRITON_REPO_ORGANIZATION:="http://github.com/triton-inference-server"} -TRITON_BACKEND_REPO_TAG=${TRITON_BACKEND_REPO_TAG:="main"} -TRITON_CORE_REPO_TAG=${TRITON_CORE_REPO_TAG:="main"} -TRITON_COMMON_REPO_TAG=${TRITON_COMMON_REPO_TAG:="main"} - -export CMAKE_POLICY_VERSION_MINIMUM=3.5 - (cd /workspace/build && \ rm -fr cc-clients java-clients python-clients && \ - export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ -DTRITON_ENABLE_CC_HTTP=ON \ -DTRITON_ENABLE_CC_GRPC=ON \ @@ -88,7 +81,6 @@ fi # (cd /workspace/build && \ rm -fr cc-clients python-clients && \ - export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ -DTRITON_ENABLE_CC_HTTP=OFF \ -DTRITON_ENABLE_CC_GRPC=ON \ @@ -116,7 +108,6 @@ fi # (cd /workspace/build && \ rm -fr cc-clients python-clients && \ - export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ -DTRITON_ENABLE_CC_HTTP=ON \ -DTRITON_ENABLE_CC_GRPC=OFF \ @@ -138,6 +129,84 @@ else exit 1 fi +# TODO: TPRD-342 These tests should be PA CI test +# cases not Triton test cases +rm -fr /workspace/build +mkdir -p /workspace/build +# +# Build without C API in Perf Analyzer +# +(cd /workspace/build && \ + cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ + -DTRITON_ENABLE_CC_HTTP=ON \ + -DTRITON_ENABLE_CC_GRPC=ON \ + -DTRITON_ENABLE_PERF_ANALYZER_C_API=OFF \ + -DTRITON_ENABLE_PERF_ANALYZER_TFS=ON \ + -DTRITON_ENABLE_PERF_ANALYZER_TS=ON \ + -DTRITON_ENABLE_GPU=ON \ + -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ + -DTRITON_COMMON_REPO_TAG=${TRITON_COMMON_REPO_TAG} \ + -DTRITON_CORE_REPO_TAG=${TRITON_CORE_REPO_TAG} \ + -DTRITON_CLIENT_REPO_TAG=${TRITON_CLIENT_REPO_TAG} \ + /workspace/perf_analyzer && \ + make -j16 perf-analyzer) +if [ $? -eq 0 ]; then + echo -e "\n***\n*** No-CAPI Passed\n***" +else + echo -e "\n***\n*** No-CAPI FAILED\n***" + exit 1 +fi + +# +# Build without TensorFlow Serving in Perf Analyzer +# +(cd /workspace/build && \ + rm -fr cc_clients perf_analyzer && \ + cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ + -DTRITON_ENABLE_CC_HTTP=ON \ + -DTRITON_ENABLE_CC_GRPC=ON \ + -DTRITON_ENABLE_PERF_ANALYZER_C_API=ON \ + -DTRITON_ENABLE_PERF_ANALYZER_TFS=OFF \ + -DTRITON_ENABLE_PERF_ANALYZER_TS=ON \ + -DTRITON_ENABLE_GPU=ON \ + -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ + -DTRITON_COMMON_REPO_TAG=${TRITON_COMMON_REPO_TAG} \ + -DTRITON_CORE_REPO_TAG=${TRITON_CORE_REPO_TAG} \ + -DTRITON_CLIENT_REPO_TAG=${TRITON_CLIENT_REPO_TAG} \ + /workspace/perf_analyzer && \ + make -j16 perf-analyzer) +if [ $? -eq 0 ]; then + echo -e "\n***\n*** No-TF-Serving Passed\n***" +else + echo -e "\n***\n*** No-TF-Serving FAILED\n***" + exit 1 +fi + +# +# Build without TorchServe in Perf Analyzer +# +(cd /workspace/build && \ + rm -fr cc_clients perf_analyzer && \ + cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ + -DTRITON_ENABLE_CC_HTTP=ON \ + -DTRITON_ENABLE_CC_GRPC=ON \ + -DTRITON_ENABLE_PERF_ANALYZER_C_API=ON \ + -DTRITON_ENABLE_PERF_ANALYZER_TFS=ON \ + -DTRITON_ENABLE_PERF_ANALYZER_TS=OFF \ + -DTRITON_ENABLE_GPU=ON \ + -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ + -DTRITON_COMMON_REPO_TAG=${TRITON_COMMON_REPO_TAG} \ + -DTRITON_CORE_REPO_TAG=${TRITON_CORE_REPO_TAG} \ + -DTRITON_CLIENT_REPO_TAG=${TRITON_CLIENT_REPO_TAG} \ + /workspace/perf_analyzer && \ + make -j16 perf-analyzer) +if [ $? -eq 0 ]; then + echo -e "\n***\n*** No-TorchServe Passed\n***" +else + echo -e "\n***\n*** No-TorchServe FAILED\n***" + exit 1 +fi + set -e echo -e "\n***\n*** Test Passed\n***" diff --git a/qa/L0_client_nobatch/test.sh b/qa/L0_client_nobatch/test.sh index 0c01bafb70..cb1a05d660 100755 --- a/qa/L0_client_nobatch/test.sh +++ b/qa/L0_client_nobatch/test.sh @@ -47,7 +47,7 @@ EXPECTED_NUM_TESTS="4" DATADIR=/data/inferenceserver/${REPO_VERSION} MODELDIR="${PWD}/qa_model_repository" -rm -rf ${MODELDIR} && mkdir -p ${MODELDIR} && cp -r ${DATADIR}/qa_model_repository/onnx_* ${MODELDIR}/. # Note there is a coupling in ./client_test.py +rm -rf ${MODELDIR} && cp -r "${DATADIR}/qa_model_repository/onnx_*" ${MODELDIR} # Note there is a coupling in ./client_test.py SERVER=/opt/tritonserver/bin/tritonserver SERVER_ARGS="--model-repository=${MODELDIR}" SERVER_LOG="./inference_server.log" diff --git a/qa/L0_client_timeout/test.sh b/qa/L0_client_timeout/test.sh index fe11549730..f250dc9fa3 100755 --- a/qa/L0_client_timeout/test.sh +++ b/qa/L0_client_timeout/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2020-2026, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2020-2021, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -241,7 +241,7 @@ wait $SERVER_PID # Test all APIs other than infer export TRITONSERVER_SERVER_DELAY_GRPC_RESPONSE_SEC=2 -SERVER_ARGS="${SERVER_ARGS} --model-control-mode=explicit --allow-client-shm=true --load-model=custom_identity_int32 --log-verbose 2" +SERVER_ARGS="${SERVER_ARGS} --model-control-mode=explicit --load-model=custom_identity_int32 --log-verbose 2" run_server if [ "$SERVER_PID" == "0" ]; then echo -e "\n***\n*** Failed to start $SERVER\n***" diff --git a/qa/L0_cuda_shared_memory/cuda_shared_memory_test.py b/qa/L0_cuda_shared_memory/cuda_shared_memory_test.py index 12ade110f0..cce6f4f63e 100755 --- a/qa/L0_cuda_shared_memory/cuda_shared_memory_test.py +++ b/qa/L0_cuda_shared_memory/cuda_shared_memory_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -153,39 +153,6 @@ def _cleanup_shm_handles(self): class CudaSharedMemoryTest(CudaSharedMemoryTestBase): - def test_client_shm_disabled_by_default(self): - # When the server is started without --allow-client-shm, registration and - # unregistration are rejected but querying status remains allowed (empty). - shm_op0_handle = cshm.create_shared_memory_region("dummy_data", 8, 0) - self._shm_handles.append(shm_op0_handle) - - shm_status_before = self.triton_client.get_cuda_shared_memory_status() - if self.protocol == "http": - self.assertEqual(len(shm_status_before), 0) - else: - self.assertEqual(len(shm_status_before.regions), 0) - - with self.assertRaisesRegex( - InferenceServerException, - "Client shared memory is disabled", - ): - self.triton_client.register_cuda_shared_memory( - "dummy_data", cshm.get_raw_handle(shm_op0_handle), 0, 8 - ) - - with self.assertRaisesRegex( - InferenceServerException, - "Client shared memory is disabled", - ): - self.triton_client.unregister_cuda_shared_memory("dummy_data") - - shm_status_after = self.triton_client.get_cuda_shared_memory_status() - self.assertEqual( - shm_status_before, - shm_status_after, - "CUDA shared memory status must be unchanged after failed register/unregister", - ) - def test_invalid_create_shm(self): # Raises error since tried to create invalid cuda shared memory region with self.assertRaisesRegex( @@ -756,11 +723,7 @@ def test_exceeds_cshm_handle_size_limit(self): try: error_message = response.json().get("error", "") self.assertIn( - "request JSON size", - error_message, - ) - self.assertIn( - "exceeds the maximum allowed input size", + "'raw_handle' exceeds the maximum allowed data size limit INT_MAX", error_message, ) except ValueError: diff --git a/qa/L0_cuda_shared_memory/test.sh b/qa/L0_cuda_shared_memory/test.sh index 1802731cd3..962aa106cb 100755 --- a/qa/L0_cuda_shared_memory/test.sh +++ b/qa/L0_cuda_shared_memory/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2026, NVIDIA CORPORATION. All rights reserved. +# Copyright 2019-2024, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -37,45 +37,6 @@ source ../common/util.sh RET=0 rm -fr *.log -# Test that shared memory registration/unregistration is rejected and status -# query is allowed when --allow-client-shm is not set (default is false). -for client_type in http grpc; do - SERVER_ARGS="--model-repository=`pwd`/models --log-verbose=1" - SERVER_LOG="./test_client_shm_disabled_by_default.$client_type.server.log" - run_server - if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 - fi - - export CLIENT_TYPE=$client_type - TEST_CLIENT_LOG="./test_client_shm_disabled_by_default.$client_type.client.log" - echo "Test: test_client_shm_disabled_by_default, client type: $client_type" >>$TEST_CLIENT_LOG - - set +e - python $SHM_TEST CudaSharedMemoryTest.test_client_shm_disabled_by_default >>$TEST_CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $TEST_CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - else - check_test_results $TEST_RESULT_FILE 1 - if [ $? -ne 0 ]; then - cat $TEST_RESULT_FILE - echo -e "\n***\n*** Test Result Verification Failed\n***" - RET=1 - fi - fi - set -e - - kill $SERVER_PID - wait $SERVER_PID -done - -# Test CUDA shared memory registration with --allow-client-shm=true -SERVER_ARGS_EXTRA="--allow-client-shm=true" - for i in \ test_invalid_create_shm \ test_valid_create_set_register \ @@ -91,7 +52,7 @@ for i in \ test_infer_offset_out_of_bound \ test_infer_byte_size_out_of_bound; do for client_type in http grpc; do - SERVER_ARGS="--model-repository=`pwd`/models --log-verbose=1 ${SERVER_ARGS_EXTRA}" + SERVER_ARGS="--model-repository=`pwd`/models --log-verbose=1" SERVER_LOG="./$i.$client_type.server.log" run_server if [ "$SERVER_PID" == "0" ]; then @@ -127,7 +88,7 @@ for i in \ test_exceeds_cshm_handle_size_limit \ test_invalid_small_cshm_handle \ test_valid_cshm_handle; do - SERVER_ARGS="--model-repository=`pwd`/models --log-verbose=1 ${SERVER_ARGS_EXTRA}" + SERVER_ARGS="--model-repository=`pwd`/models --log-verbose=1" SERVER_LOG="./$i.server.log" CLIENT_LOG="./$i.client.log" run_server diff --git a/qa/L0_decoupled/decoupled_test.py b/qa/L0_decoupled/decoupled_test.py index edb5d8c48a..d7bc59f5c7 100755 --- a/qa/L0_decoupled/decoupled_test.py +++ b/qa/L0_decoupled/decoupled_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2020-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -32,7 +32,6 @@ import os import queue -import threading import time import unittest from functools import partial @@ -607,212 +606,53 @@ def test_wrong_shape(self): class NonDecoupledTest(tu.TestResultCollector): def setUp(self): self.model_name_ = "repeat_int32" - self.data_matrix = [ - # ("IN", "DELAY", "WAIT") - ([1], [0], [0]), - ([1], [4000], [2000]), - ([1], [2000], [4000]), - ] - - # For grpc async infer test - self.callback_error = None - self.callback_result = None - self.callback_invoked_event = threading.Event() - - def _input_data(self, in_value, delay_value, wait_value): - return { - "IN": np.array(in_value, dtype=np.int32), - "DELAY": np.array(delay_value, dtype=np.uint32), - "WAIT": np.array(wait_value, dtype=np.uint32), + self.input_data = { + "IN": np.array([1], dtype=np.int32), + "DELAY": np.array([0], dtype=np.uint32), + "WAIT": np.array([0], dtype=np.uint32), } - def _async_callback(self, result, error): - """Callback for async_infer.""" - self.callback_error = error - self.callback_result = result - self.callback_invoked_event.set() - def test_grpc(self): - for in_value, delay_value, wait_value in self.data_matrix: - with self.subTest(IN=in_value, DELAY=delay_value, WAIT=wait_value): - input_data = self._input_data(in_value, delay_value, wait_value) - inputs = [ - grpcclient.InferInput("IN", [1], "INT32").set_data_from_numpy( - input_data["IN"] - ), - grpcclient.InferInput("DELAY", [1], "UINT32").set_data_from_numpy( - input_data["DELAY"] - ), - grpcclient.InferInput("WAIT", [1], "UINT32").set_data_from_numpy( - input_data["WAIT"] - ), - ] - - triton_client = grpcclient.InferenceServerClient( - url="localhost:8001", verbose=True - ) + inputs = [ + grpcclient.InferInput("IN", [1], "INT32").set_data_from_numpy( + self.input_data["IN"] + ), + grpcclient.InferInput("DELAY", [1], "UINT32").set_data_from_numpy( + self.input_data["DELAY"] + ), + grpcclient.InferInput("WAIT", [1], "UINT32").set_data_from_numpy( + self.input_data["WAIT"] + ), + ] - # Expect the inference is successful - res = triton_client.infer(model_name=self.model_name_, inputs=inputs) - self.assertEqual(1, res.as_numpy("OUT")[0]) - self.assertEqual(0, res.as_numpy("IDX")[0]) + triton_client = grpcclient.InferenceServerClient( + url="localhost:8001", verbose=True + ) + # Expect the inference is successful + res = triton_client.infer(model_name=self.model_name_, inputs=inputs) + self.assertEqual(1, res.as_numpy("OUT")[0]) + self.assertEqual(0, res.as_numpy("IDX")[0]) def test_http(self): - for in_value, delay_value, wait_value in self.data_matrix: - with self.subTest(IN=in_value, DELAY=delay_value, WAIT=wait_value): - input_data = self._input_data(in_value, delay_value, wait_value) - inputs = [ - httpclient.InferInput("IN", [1], "INT32").set_data_from_numpy( - input_data["IN"] - ), - httpclient.InferInput("DELAY", [1], "UINT32").set_data_from_numpy( - input_data["DELAY"] - ), - httpclient.InferInput("WAIT", [1], "UINT32").set_data_from_numpy( - input_data["WAIT"] - ), - ] - - triton_client = httpclient.InferenceServerClient( - url="localhost:8000", verbose=True - ) - - # Expect the inference is successful - res = triton_client.infer(model_name=self.model_name_, inputs=inputs) - self.assertEqual(1, res.as_numpy("OUT")[0]) - self.assertEqual(0, res.as_numpy("IDX")[0]) - - def test_grpc_async(self): - for in_value, delay_value, wait_value in self.data_matrix: - with self.subTest(IN=in_value, DELAY=delay_value, WAIT=wait_value): - input_data = self._input_data(in_value, delay_value, wait_value) - inputs = [ - grpcclient.InferInput("IN", [1], "INT32").set_data_from_numpy( - input_data["IN"] - ), - grpcclient.InferInput("DELAY", [1], "UINT32").set_data_from_numpy( - input_data["DELAY"] - ), - grpcclient.InferInput("WAIT", [1], "UINT32").set_data_from_numpy( - input_data["WAIT"] - ), - ] - - triton_client = grpcclient.InferenceServerClient( - url="localhost:8001", - verbose=True, - ) - - # Clear previous results - self.callback_error = None - self.callback_result = None - self.callback_invoked_event.clear() - - try: - triton_client.async_infer( - model_name=self.model_name_, - inputs=inputs, - callback=self._async_callback, - ) - except Exception as e: - self.fail(f"Failed to initiate async_infer: {e}") - continue - - # Wait for the callback to be invoked, with a timeout - self.assertTrue( - self.callback_invoked_event.wait(timeout=10), - "Callback not invoked within timeout.", - ) - - # Expect the inference is successful - self.assertIsNone( - self.callback_error, f"Inference failed: {self.callback_error}" - ) - self.assertIsNotNone(self.callback_result, "Inference result is None.") - self.assertEqual(1, self.callback_result.as_numpy("OUT")[0]) - self.assertEqual(0, self.callback_result.as_numpy("IDX")[0]) - - # Wait and check server/model health - time.sleep(5) - self.assertTrue(triton_client.is_model_ready(self.model_name_)) - - def test_grpc_async_cancel(self): - data_matrix = [ - # ("IN", "DELAY", "WAIT") - ([1], [4000], [2000]), - ([1], [2000], [4000]), + inputs = [ + httpclient.InferInput("IN", [1], "INT32").set_data_from_numpy( + self.input_data["IN"] + ), + httpclient.InferInput("DELAY", [1], "UINT32").set_data_from_numpy( + self.input_data["DELAY"] + ), + httpclient.InferInput("WAIT", [1], "UINT32").set_data_from_numpy( + self.input_data["WAIT"] + ), ] - for in_value, delay_value, wait_value in data_matrix: - with self.subTest(IN=in_value, DELAY=delay_value, WAIT=wait_value): - input_data = self._input_data(in_value, delay_value, wait_value) - inputs = [ - grpcclient.InferInput("IN", [1], "INT32").set_data_from_numpy( - input_data["IN"] - ), - grpcclient.InferInput("DELAY", [1], "UINT32").set_data_from_numpy( - input_data["DELAY"] - ), - grpcclient.InferInput("WAIT", [1], "UINT32").set_data_from_numpy( - input_data["WAIT"] - ), - ] - - triton_client = grpcclient.InferenceServerClient( - url="localhost:8001", - verbose=True, - ) - - # Clear previous results - self.callback_error = None - self.callback_result = None - self.callback_invoked_event.clear() - - request_handle = None - try: - request_handle = triton_client.async_infer( - model_name=self.model_name_, - inputs=inputs, - callback=self._async_callback, - ) - except Exception as e: - self.fail(f"Failed to initiate async_infer: {e}") - continue - - # Allow request to be fully initiated - time.sleep(0.5) - - # Attempt to cancel the request - if request_handle: - try: - request_handle.cancel() - except Exception as e: - self.fail(f"Error calling request_handle.cancel(): {e}") - continue - else: - self.fail("Invalid request_handle, cannot cancel.") - continue - - # Wait for the callback to be invoked - self.assertTrue( - self.callback_invoked_event.wait(timeout=10), - "Callback not invoked within timeout after cancellation.", - ) - - # Expect the inference is failed - self.assertIsInstance( - self.callback_error, - InferenceServerException, - f"Unexpected error type: {type(self.callback_error)}", - ) - self.assertIn( - "StatusCode.CANCELLED", - self.callback_error.status(), - ) - - # Wait and check server/model health - time.sleep(5) - self.assertTrue(triton_client.is_model_ready(self.model_name_)) + triton_client = httpclient.InferenceServerClient( + url="localhost:8000", verbose=True + ) + # Expect the inference is successful + res = triton_client.infer(model_name=self.model_name_, inputs=inputs) + self.assertEqual(1, res.as_numpy("OUT")[0]) + self.assertEqual(0, res.as_numpy("IDX")[0]) if __name__ == "__main__": diff --git a/qa/L0_decoupled/test.sh b/qa/L0_decoupled/test.sh index 839d0131aa..20ca0fffa3 100755 --- a/qa/L0_decoupled/test.sh +++ b/qa/L0_decoupled/test.sh @@ -196,7 +196,7 @@ if [ $? -ne 0 ]; then echo -e "\n***\n*** Test NonDecoupledTest Failed\n***" RET=1 else - check_test_results $TEST_RESULT_FILE 4 + check_test_results $TEST_RESULT_FILE 2 if [ $? -ne 0 ]; then cat $CLIENT_LOG echo -e "\n***\n*** Test Result Verification Failed\n***" diff --git a/qa/L0_dlpack_multi_gpu/test.sh b/qa/L0_dlpack_multi_gpu/test.sh index 0aaf135b5f..d85a089431 100755 --- a/qa/L0_dlpack_multi_gpu/test.sh +++ b/qa/L0_dlpack_multi_gpu/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -41,22 +41,10 @@ source ../common/util.sh # Uninstall the non CUDA version of PyTorch pip3 uninstall -y torch -pip3 install torch -f https://download.pytorch.org/whl/cu130 +pip3 install torch==2.3.1+cu118 -f https://download.pytorch.org/whl/torch_stable.html # Install CuPy for testing non_blocking compute streams -pip3 install cupy-cuda13x - -if [ ${CUDA_VERSION%%.*} -gt 12 ]; then - curl -L https://developer.download.nvidia.com/compute/cuda/redist/cuda_nvrtc/linux-x86_64/cuda_nvrtc-linux-x86_64-12.9.86-archive.tar.xz \ - -o /tmp/cuda_nvrtc-linux-x86_64-12.9.86-archive.tar.xz ; - curl -L https://developer.download.nvidia.com/compute/cuda/redist/libcublas/linux-x86_64/libcublas-linux-x86_64-12.9.1.4-archive.tar.xz \ - -o /tmp/libcublas-linux-x86_64-12.9.1.4-archive.tar.xz ; - cd /tmp ; - tar -xvf /tmp/cuda_nvrtc-linux-x86_64-12.9.86-archive.tar.xz --strip-components=1 ; - tar -xvf /tmp/libcublas-linux-x86_64-12.9.1.4-archive.tar.xz --strip-components=1 ; - export LD_LIBRARY_PATH=/tmp/lib:$LD_LIBRARY_PATH ; - cd - -fi +pip3 install cupy-cuda12x rm -fr *.log ./models diff --git a/qa/L0_grpc/python_grpc_aio_test.py b/qa/L0_grpc/python_grpc_aio_test.py index 6466432c6b..ba43b36abb 100755 --- a/qa/L0_grpc/python_grpc_aio_test.py +++ b/qa/L0_grpc/python_grpc_aio_test.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -100,11 +100,9 @@ async def test_get_system_shared_memory_status(self): async def test_register_system_shared_memory(self): with self.assertRaisesRegex( InferenceServerException, - "\[StatusCode\.INTERNAL\] Unable to open shared memory region: '/test_shm'", + "\[StatusCode\.INTERNAL\] Unable to open shared memory region: ''", ): - await self._triton_client.register_system_shared_memory( - "test_shm", "/test_shm", 0 - ) + await self._triton_client.register_system_shared_memory("", "", 0) async def test_unregister_system_shared_memory(self): await self._triton_client.unregister_system_shared_memory() diff --git a/qa/L0_grpc/python_unit_test.py b/qa/L0_grpc/python_unit_test.py index 5ed87078cf..9591d4274c 100755 --- a/qa/L0_grpc/python_unit_test.py +++ b/qa/L0_grpc/python_unit_test.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -34,10 +34,7 @@ import numpy as np import tritonclient.grpc as grpcclient -from tritonclient.grpc import service_pb2, service_pb2_grpc -from tritonclient.utils import InferenceServerException, deserialize_bytes_tensor - -import grpc +from tritonclient.utils import InferenceServerException class UserData: @@ -52,90 +49,6 @@ def callback(user_data, result, error): user_data._completed_requests.put(result) -class GrpcTest(unittest.TestCase): - def test_duplicate_output_names_rejected(self): - """Test that duplicate output names in a gRPC infer request are rejected.""" - client = grpcclient.InferenceServerClient(url="localhost:8001") - inputs = [ - grpcclient.InferInput("INPUT0", [1, 16], "INT32"), - grpcclient.InferInput("INPUT1", [1, 16], "INT32"), - ] - inputs[0].set_data_from_numpy(np.ones(shape=(1, 16), dtype=np.int32)) - inputs[1].set_data_from_numpy(np.ones(shape=(1, 16), dtype=np.int32)) - - num_duplicates = 2 - outputs = [ - grpcclient.InferRequestedOutput("OUTPUT0") for _ in range(num_duplicates) - ] - - with self.assertRaises(InferenceServerException) as ctx: - client.infer(model_name="simple", inputs=inputs, outputs=outputs) - self.assertIn( - "output 'OUTPUT0' already exists in request", - str(ctx.exception), - ) - - self.assertTrue( - client.is_server_live(), - "Server is not healthy after duplicate output request", - ) - - def test_bytes_contents_many_elements_serialization(self): - """ - Regression test for InferGRPCToInput bytes_contents pre-allocation. - Sends a BYTES tensor with many explicit bytes_contents elements over - the raw gRPC stub. - """ - # 10,485,760 elements * (4-byte length + 1-byte payload) = 50 MiB - # of serialized BYTES data on both the request and response sides. - element_count = 10 * 1024 * 1024 - payload = b"A" - expected_serialized_size = element_count * (4 + len(payload)) - - request = service_pb2.ModelInferRequest() - request.model_name = "string_identity" - - input_tensor = request.inputs.add() - input_tensor.name = "INPUT0" - input_tensor.datatype = "BYTES" - input_tensor.shape.extend([element_count]) - input_tensor.contents.bytes_contents.extend([payload] * element_count) - request.outputs.add().name = "OUTPUT0" - - # The default Python gRPC client send/receive limit is 4 MiB which - # is below the request and response sizes used here. - channel_options = [ - ("grpc.max_send_message_length", 256 * 1024 * 1024), - ("grpc.max_receive_message_length", 256 * 1024 * 1024), - ] - grpc_stub = service_pb2_grpc.GRPCInferenceServiceStub( - grpc.insecure_channel("localhost:8001", options=channel_options) - ) - response = grpc_stub.ModelInfer(request, timeout=120) - - self.assertEqual(list(response.outputs[0].shape), [element_count]) - self.assertEqual(len(response.raw_output_contents[0]), expected_serialized_size) - - # Verify every element round-tripped correctly through the - # serialization path. - output = deserialize_bytes_tensor(response.raw_output_contents[0]) - self.assertEqual(output.shape, (element_count,)) - self.assertTrue( - np.all(output == payload), - "bytes_contents elements did not round-trip correctly", - ) - - client = grpcclient.InferenceServerClient(url="localhost:8001") - self.assertTrue( - client.is_server_live(), - "Server must remain healthy after many bytes_contents elements", - ) - self.assertTrue( - client.is_model_ready("string_identity"), - "Model must remain ready after many bytes_contents elements", - ) - - class RestrictedProtocolTest(unittest.TestCase): def setUp(self): self.client_ = grpcclient.InferenceServerClient(url="localhost:8001") diff --git a/qa/L0_grpc/test.sh b/qa/L0_grpc/test.sh index c17c3db30c..28d9236d0a 100755 --- a/qa/L0_grpc/test.sh +++ b/qa/L0_grpc/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -152,25 +152,13 @@ sed -i "/CONTROL_SEQUENCE_CORRID/{n;s/data_type:.*/data_type: TYPE_STRING/}" ${M rm -f ${MODELDIR}/simple_string_dyna_sequence/1/model.onnx cp ../custom_models/custom_dyna_sequence_int32/1/libtriton_dyna_sequence.so ${MODELDIR}/simple_string_dyna_sequence/1/ -# Prepare a dedicated model repository for GrpcTest in python_unit_test.py. -# separate from MODELDIR avoids changing the model count expected by other -# tests in this script. -GRPC_TEST_MODELDIR=`pwd`/grpc_test_models -rm -rf ${GRPC_TEST_MODELDIR} -mkdir -p ${GRPC_TEST_MODELDIR} -cp -r ${MODELDIR}/simple ${GRPC_TEST_MODELDIR}/ -cp -r ../python_models/string_identity ${GRPC_TEST_MODELDIR}/string_identity -mkdir -p ${GRPC_TEST_MODELDIR}/string_identity/1 -mv ${GRPC_TEST_MODELDIR}/string_identity/model.py ${GRPC_TEST_MODELDIR}/string_identity/1/model.py -sed -i "s/dims: \[ 1 \]/dims: [ -1 ]/g" ${GRPC_TEST_MODELDIR}/string_identity/config.pbtxt - rm -f *.log rm -f *.log.* set -e CLIENT_LOG=`pwd`/client.log -SERVER_ARGS="--backend-directory=${BACKEND_DIR} --model-repository=${MODELDIR} --allow-client-shm=true" +SERVER_ARGS="--backend-directory=${BACKEND_DIR} --model-repository=${MODELDIR}" source ../common/util.sh run_server @@ -217,19 +205,19 @@ for i in \ EXTRA_ARGS="-i grpc -u localhost:8001" fi if [[ ($SUFFIX == "image_client") || ($SUFFIX == "grpc_image_client") ]]; then - python $i -m densenet_onnx -s INCEPTION -a -c 1 -b 1 $EXTRA_ARGS $IMAGE >> "${CLIENT_LOG}.async.${SUFFIX}" 2>&1 + python $i -m inception_onnx -s INCEPTION -a -c 1 -b 1 $EXTRA_ARGS $IMAGE >> "${CLIENT_LOG}.async.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.async.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.async.${SUFFIX} RET=1 fi - python $i -m densenet_onnx -s INCEPTION -a --streaming -c 1 -b 1 $EXTRA_ARGS $IMAGE >> "${CLIENT_LOG}.streaming.${SUFFIX}" 2>&1 + python $i -m inception_onnx -s INCEPTION -a --streaming -c 1 -b 1 $EXTRA_ARGS $IMAGE >> "${CLIENT_LOG}.streaming.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.streaming.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.streaming.${SUFFIX} RET=1 fi - python $i -m densenet_onnx -s INCEPTION -c 1 -b 1 $EXTRA_ARGS $IMAGE >> "${CLIENT_LOG}.${SUFFIX}" 2>&1 + python $i -m inception_onnx -s INCEPTION -c 1 -b 1 $EXTRA_ARGS $IMAGE >> "${CLIENT_LOG}.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.${SUFFIX} @@ -282,19 +270,19 @@ for i in \ BASE=$(basename -- $i) SUFFIX="${BASE%.*}" if [[ $SUFFIX == "image_client" ]]; then - $i -m densenet_onnx -s INCEPTION -a -c 1 -b 1 -i grpc -u localhost:8001 $IMAGE >> "${CLIENT_LOG}.c++.async.${SUFFIX}" 2>&1 + $i -m inception_onnx -s INCEPTION -a -c 1 -b 1 -i grpc -u localhost:8001 $IMAGE >> "${CLIENT_LOG}.c++.async.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.c++.async.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.c++.${SUFFIX} RET=1 fi - $i -m densenet_onnx -s INCEPTION -a --streaming -c 1 -b 1 -i grpc -u localhost:8001 $IMAGE >> "${CLIENT_LOG}.c++.streaming.${SUFFIX}" 2>&1 + $i -m inception_onnx -s INCEPTION -a --streaming -c 1 -b 1 -i grpc -u localhost:8001 $IMAGE >> "${CLIENT_LOG}.c++.streaming.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.c++.streaming.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.c++.${SUFFIX} RET=1 fi - $i -m densenet_onnx -s INCEPTION -c 1 -b 1 -i grpc -u localhost:8001 $IMAGE >> "${CLIENT_LOG}.c++.${SUFFIX}" 2>&1 + $i -m inception_onnx -s INCEPTION -c 1 -b 1 -i grpc -u localhost:8001 $IMAGE >> "${CLIENT_LOG}.c++.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.c++.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.c++.${SUFFIX} @@ -365,29 +353,6 @@ set -e kill $SERVER_PID wait $SERVER_PID -# Test duplicate output and bytes_contents -SERVER_ARGS="--backend-directory=${BACKEND_DIR} --model-repository=${GRPC_TEST_MODELDIR}" -SERVER_LOG=./inference_server_grpc_test.log -GRPC_TEST_CLIENT_LOG=./inference_client_grpc_test.log -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 -fi - -set +e -python $PYTHON_UNIT_TEST GrpcTest >> $GRPC_TEST_CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $GRPC_TEST_CLIENT_LOG - echo -e "\n***\n*** Python GRPC Test Failed\n***" - RET=1 -fi - -set -e -kill $SERVER_PID -wait $SERVER_PID - SERVER_ARGS="--backend-directory=${BACKEND_DIR} --model-repository=${CLIENT_PLUGIN_MODELDIR} --http-header-forward-pattern=.* --grpc-header-forward-pattern=.*" run_server if [ "$SERVER_PID" == "0" ]; then @@ -604,7 +569,7 @@ done # Run python grpc aio unit test PYTHON_GRPC_AIO_TEST=python_grpc_aio_test.py CLIENT_LOG=`pwd`/python_grpc_aio_test.log -SERVER_ARGS="--backend-directory=${BACKEND_DIR} --model-repository=${MODELDIR} --allow-client-shm=true" +SERVER_ARGS="--backend-directory=${BACKEND_DIR} --model-repository=${MODELDIR}" run_server if [ "$SERVER_PID" == "0" ]; then echo -e "\n***\n*** Failed to start $SERVER\n***" diff --git a/qa/L0_http/generate_endpoint_test.py b/qa/L0_http/generate_endpoint_test.py index df9317b4dd..3eb0b6ea5f 100755 --- a/qa/L0_http/generate_endpoint_test.py +++ b/qa/L0_http/generate_endpoint_test.py @@ -1,5 +1,5 @@ #!/usr/bin/python3 -# Copyright 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -29,7 +29,6 @@ sys.path.append("../common") -import base64 import json import threading import time @@ -275,37 +274,6 @@ def test_invalid_input_types(self): self.generate_expect_failure(self._model_name, inputs, error_msg) self.generate_stream_expect_failure(self._model_name, inputs, error_msg) - def test_json_dtype_size_expansion_exceeds_limit_error(self): - """ - Test that when the client sends a JSON input of byte[], that when it - expands to dtype[], it exceeds the maximum allowed input size and - returns an appropriate error message. The test sends a large base64 - encoded string as input, which simulates a byte[] input that would - expand to a much larger dtype[] input on the server side when - `sizeof(dtype) > 1`. - The test checks that the error message indicates that the input size - exceeds the limit. - This is important to prevent clients from sending inputs that could - cause excessive memory usage on the server. - """ - - input_data = [1] * ( - 64 * 1024 * 1024 - ) # 64MB input, which is large but still reasonable for HTTP request body - input_bytes = bytes(input_data) - input_str = base64.b64encode(input_bytes).decode("utf-8") - inputs = {"PROMPT": input_str, "STREAM": False} - error_msg = " bytes exceeds the maximum allowed input size of " - self.generate_expect_failure(self._model_name, inputs, error_msg) - - inputs = { - "INPUT0": input_str[0 : (len(input_str) // 2)], - "INPUT1": input_str[(len(input_str) // 2) :], - "STREAM": False, - } - error_msg = " bytes exceeds the maximum allowed input size of " - self.generate_expect_failure(self._model_name, inputs, error_msg) - def test_duplicate_inputs(self): dupe_prompt = "input 'PROMPT' already exists in request" dupe_stream = "input 'STREAM' already exists in request" diff --git a/qa/L0_http/http_input_size_limit_test.py b/qa/L0_http/http_input_size_limit_test.py deleted file mode 100755 index aed855dd69..0000000000 --- a/qa/L0_http/http_input_size_limit_test.py +++ /dev/null @@ -1,796 +0,0 @@ -#!/usr/bin/python -# Copyright 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import sys - -sys.path.append("../common") - -import base64 -import gzip -import io -import json -import unittest - -import numpy as np -import requests -from test_util import GIB, MIB, TestResultCollector, get_server_process_from_env - -# Constants for size calculations -# Each FP32 value is 4 bytes, so we need to divide target byte sizes by 4 to get element counts -BYTES_PER_FP32 = 4 -BYTES_PER_INT64 = ( - 8 # For the type size explosion test, we use int64 which is 8 bytes per element -) -DEFAULT_LIMIT_BYTES = 64 * MIB # 64MB default limit -INCREASED_LIMIT_BYTES = 128 * MIB # 128MB increased limit - -# Calculate element counts for size limits -DEFAULT_LIMIT_ELEMENTS = DEFAULT_LIMIT_BYTES // BYTES_PER_FP32 # 16,777,216 elements -INCREASED_LIMIT_ELEMENTS = ( - INCREASED_LIMIT_BYTES // BYTES_PER_FP32 -) # 33,554,432 elements - -# Small offsets to go just over/under the limits -OFFSET_ELEMENTS = 32 - - -class InferSizeLimitTest(TestResultCollector): - def _get_infer_url(self, model_name): - return f"http://localhost:8000/v2/models/{model_name}/infer" - - def test_json_dtype_size_expansion_exceeds_limit_error(self): - """ - Test that when the client sends a JSON input of byte[], that when it - expands to dtype[], it exceeds the maximum allowed input size and - returns an appropriate error message. The test sends a large base64 - encoded string as input, which simulates a byte[] input that would - expand to a much larger dtype[] input on the server side when - `sizeof(dtype) > 1`. - The test checks that the error message indicates that the input size - exceeds the limit. - This is important to prevent clients from sending inputs that could - cause excessive memory usage on the server. - """ - model = "onnx_zero_1_float32" - - # Provided data is 64MB of int8, but the model expects FP32, - # which would expand to 256MB when interpreted as FP32. - bytes_input = np.ones(DEFAULT_LIMIT_BYTES, dtype=np.int8) - input_bytes = bytes_input.tobytes() - data_str = base64.b64encode(input_bytes).decode("utf-8") - headers = { - "Content-Type": "application/json", - "Inference-Header-Content-Length": f"{len(input_bytes)}", - } - shape_size = ( - DEFAULT_LIMIT_ELEMENTS // BYTES_PER_INT64 - ) # Calculate shape size based on int64 element count to match the byte size - - payload = { - "inputs": [ - { - "name": "INPUT0", - "datatype": "INT64", - "shape": [1, shape_size], - "data": data_str, - } - ] - } - - response = requests.post( - f"http://localhost:8000/v2/models/{model}/generate", - headers=headers, - json=payload, - ) - - self.assertEqual( - 400, - response.status_code, - f"Expected error code for type/size mismatch, got: {response.status_code}", - ) - error_msg = response.content.decode() - print( - f"Error message: {error_msg}", flush=True - ) # Print the error message for debugging - self.assertIn( - "request JSON size of ", - error_msg, - ) - self.assertIn( - " bytes exceeds the maximum allowed input size of ", - error_msg, - ) - self.assertIn( - "Use --http-max-input-size to increase the limit.", - error_msg, - ) - - # Test multiple inputs with one that causes size explosion. - payload = { - "inputs": [ - { - "name": "INPUT0", - "datatype": "INT64", - "shape": [1, shape_size // 2], - "data": data_str[: len(data_str) // 2], - }, - { - "name": "INPUT1", - "datatype": "INT64", - "shape": [1, shape_size // 2], - "data": data_str[len(data_str) // 2 :], - }, - ] - } - - response = requests.post( - f"http://localhost:8000/v2/models/{model}/generate", - headers=headers, - json=payload, - ) - - self.assertEqual( - 400, - response.status_code, - f"Expected error code for type/size mismatch, got: {response.status_code}", - ) - error_msg = response.content.decode() - print( - f"Error message: {error_msg}", flush=True - ) # Print the error message for debugging - self.assertIn( - "request JSON size of ", - error_msg, - ) - self.assertIn( - " bytes exceeds the maximum allowed input size of ", - error_msg, - ) - self.assertIn( - "Use --http-max-input-size to increase the limit.", - error_msg, - ) - - def test_default_limit_raw_binary(self): - """Test raw binary inputs with default limit""" - model = "onnx_zero_1_float32" - - # Test case 1: Input just over the 64MB limit (should fail) - # (2^24 + 32) elements * 4 bytes = 64MB + 128 bytes = 67,108,992 bytes - large_input = np.ones( - DEFAULT_LIMIT_ELEMENTS + OFFSET_ELEMENTS, dtype=np.float32 - ) - input_bytes = large_input.tobytes() - assert len(input_bytes) > 64 * MIB # Verify we're actually over the 64MB limit - - headers = {"Inference-Header-Content-Length": "0"} - response = requests.post( - self._get_infer_url(model), data=input_bytes, headers=headers - ) - - # Should fail with 400 bad request with default limit - self.assertEqual( - 400, - response.status_code, - "Expected error code for oversized request, got: {}".format( - response.status_code - ), - ) - - # Verify error message contains size limit info - error_msg = response.content.decode() - self.assertIn( - "exceeds the maximum allowed value", - error_msg, - "Expected error message about exceeding max input size", - ) - - # Test case 2: Input just under the 64MB limit (should succeed) - # (2^24 - 32) elements * 4 bytes = 64MB - 128 bytes = 67,108,736 bytes - small_input = np.ones( - DEFAULT_LIMIT_ELEMENTS - OFFSET_ELEMENTS, dtype=np.float32 - ) - input_bytes = small_input.tobytes() - assert len(input_bytes) < 64 * MIB # Verify we're actually under the 64MB limit - - response = requests.post( - self._get_infer_url(model), data=input_bytes, headers=headers - ) - - # Should succeed with 200 OK - self.assertEqual( - 200, - response.status_code, - "Expected success code for request within size limit, got: {}".format( - response.status_code - ), - ) - - # Verify output matches our input (identity model) - header_size = int(response.headers["Inference-Header-Content-Length"]) - output_data = response.content[header_size:] - - # Convert output bytes back to numpy array for comparison - output_array = np.frombuffer(output_data, dtype=np.float32) - self.assertTrue( - np.array_equal(output_array, small_input), - "Response data does not match input data", - ) - - def test_default_limit_json(self): - """Test JSON inputs with default limit""" - model = "onnx_zero_1_float32" - - # Test case 1: Input just over the 64MB limit (should fail) - # (2^24 + 32) elements * 4 bytes = 64MB + 128 bytes = 67,108,992 bytes - shape_size = DEFAULT_LIMIT_ELEMENTS + OFFSET_ELEMENTS - - payload = { - "inputs": [ - { - "name": "INPUT0", - "datatype": "FP32", - "shape": [1, shape_size], - "data": [1.0] * shape_size, - } - ] - } - assert ( - shape_size * BYTES_PER_FP32 > 64 * MIB - ) # Verify we're actually over the 64MB limit - - headers = {"Content-Type": "application/json"} - response = requests.post( - self._get_infer_url(model), headers=headers, json=payload - ) - - # Should fail with 400 bad request with default limit - self.assertEqual( - 400, - response.status_code, - "Expected error code for oversized JSON request, got: {}".format( - response.status_code - ), - ) - - # Verify error message contains size limit info - error_msg = response.content.decode() - self.assertIn( - "request JSON size of ", - error_msg, - ) - self.assertIn( - " bytes exceeds the maximum allowed input size of ", - error_msg, - ) - self.assertIn( - "Use --http-max-input-size to increase the limit.", - error_msg, - ) - - # Test case 2: Input just under the 64MB limit (should succeed) - # The test creates a JSON payload with data, which adds overhead compared - # to raw binary format. We adjust the shape size to ensure the final - # JSON payload is under the size limit. An element is roughly 5 - # bytes in JSON, compared to 4 bytes as a raw FP32. - shape_size = (DEFAULT_LIMIT_ELEMENTS - OFFSET_ELEMENTS) * 4 // 5 - - payload = { - "inputs": [ - { - "name": "INPUT0", - "datatype": "FP32", - "shape": [1, shape_size], - "data": [1.0] * shape_size, - } - ] - } - # Verify we're actually under the 64MB limit - self.assertLess(len(json.dumps(payload).encode("utf-8")), DEFAULT_LIMIT_BYTES) - - response = requests.post( - self._get_infer_url(model), headers=headers, json=payload - ) - - # Should succeed with 200 OK - self.assertEqual( - 200, - response.status_code, - "Expected success code for JSON request within size limit, got: {}".format( - response.status_code - ), - ) - - # Verify we got a valid response - result = response.json() - self.assertIn("outputs", result, "Response missing outputs field") - self.assertEqual(1, len(result["outputs"]), "Expected 1 output") - self.assertEqual( - shape_size, - result["outputs"][0]["shape"][1], - f"Expected shape {[1, shape_size]}, got {result['outputs'][0]['shape']}", - ) - - def test_large_input_raw_binary(self): - """Test raw binary input larger with custom limit set""" - model = "onnx_zero_1_float32" - - # Test case 1: Input just over the 128MB configured limit (should fail) - # (2^25 + 32) elements * 4 bytes = 128MB + 128 bytes = 134,217,856 bytes - large_input = np.ones( - INCREASED_LIMIT_ELEMENTS + OFFSET_ELEMENTS, dtype=np.float32 - ) - input_bytes = large_input.tobytes() - assert ( - len(input_bytes) > 128 * MIB - ) # Verify we're actually over the 128MB limit - - headers = {"Inference-Header-Content-Length": "0"} - response = requests.post( - self._get_infer_url(model), data=input_bytes, headers=headers - ) - - # Should fail with 400 bad request with our increased limit - self.assertEqual( - 400, - response.status_code, - "Expected error code for oversized request, got: {}".format( - response.status_code - ), - ) - - # Verify error message contains size limit info - error_msg = response.content.decode() - self.assertIn( - "exceeds the maximum allowed value", - error_msg, - "Expected error message about exceeding max input size", - ) - - # Test case 2: Input just under the 128MB configured limit (should succeed) - # (2^25 - 32) elements * 4 bytes = 128MB - 128 bytes = 134,217,600 bytes - small_input = np.ones( - INCREASED_LIMIT_ELEMENTS - OFFSET_ELEMENTS, dtype=np.float32 - ) - input_bytes = small_input.tobytes() - assert ( - len(input_bytes) < 128 * MIB - ) # Verify we're actually under the 128MB limit - - response = requests.post( - self._get_infer_url(model), data=input_bytes, headers=headers - ) - - # Should succeed with 200 OK - self.assertEqual( - 200, - response.status_code, - "Expected success code for request within increased limit, got: {}".format( - response.status_code - ), - ) - - # Verify output matches our input (identity model) - header_size = int(response.headers["Inference-Header-Content-Length"]) - output_data = response.content[header_size:] - - # Convert output bytes back to numpy array for comparison - output_array = np.frombuffer(output_data, dtype=np.float32) - self.assertTrue( - np.array_equal(output_array, small_input), - "Response data does not match input data", - ) - - def test_large_input_json(self): - """Test JSON input larger with custom limit set""" - model = "onnx_zero_1_float32" - - # Test case 1: Input just over the 128MB configured limit (should fail) - # (2^25 + 32) elements * 4 bytes = 128MB + 128 bytes = 134,217,856 bytes - shape_size = INCREASED_LIMIT_ELEMENTS + OFFSET_ELEMENTS - - payload = { - "inputs": [ - { - "name": "INPUT0", - "datatype": "FP32", - "shape": [1, shape_size], - "data": [1.0] * shape_size, - } - ] - } - assert ( - shape_size * BYTES_PER_FP32 > 128 * MIB - ) # Verify we're actually over the 128MB limit - - headers = {"Content-Type": "application/json"} - response = requests.post( - self._get_infer_url(model), headers=headers, json=payload - ) - - # Should fail with 400 bad request with our increased limit - self.assertEqual( - 400, - response.status_code, - "Expected error code for oversized JSON request, got: {}".format( - response.status_code - ), - ) - - # Verify error message contains size limit info - error_msg = response.content.decode() - self.assertIn( - "request JSON size of ", - error_msg, - ) - self.assertIn( - " bytes exceeds the maximum allowed input size of ", - error_msg, - ) - self.assertIn( - "Use --http-max-input-size to increase the limit.", - error_msg, - ) - - # Test case 2: Input just under the 128MB configured limit (should succeed) - # The test creates a JSON payload with data, which adds overhead compared - # to raw binary format. We adjust the shape size to ensure the final - # JSON payload is under the size limit. An element is roughly 5 - # bytes in JSON, compared to 4 bytes as a raw FP32. - shape_size = (INCREASED_LIMIT_ELEMENTS - OFFSET_ELEMENTS) * 4 // 5 - - payload = { - "inputs": [ - { - "name": "INPUT0", - "datatype": "FP32", - "shape": [1, shape_size], - "data": [1.0] * shape_size, - } - ] - } - # Verify we're actually under the 128MB limit - self.assertLess(len(json.dumps(payload).encode("utf-8")), INCREASED_LIMIT_BYTES) - - response = requests.post( - self._get_infer_url(model), headers=headers, json=payload - ) - - # Should succeed with 200 OK - self.assertEqual( - 200, - response.status_code, - "Expected success code for request within increased limit, got: {}".format( - response.status_code - ), - ) - - # Verify we got a valid response - result = response.json() - self.assertIn("outputs", result, "Response missing outputs field") - self.assertEqual(1, len(result["outputs"]), "Expected 1 output") - self.assertEqual( - shape_size, - result["outputs"][0]["shape"][1], - f"Expected shape {[1, shape_size]}, got {result['outputs'][0]['shape']}", - ) - - def test_large_string_in_json(self): - """Test JSON request with large string input""" - model = "simple_identity" - - # Create a string that is larger (large payload about 2GB) than the default limit of 64MB - # (2^31 + 64) elements * 1 bytes = 2GB + 64 bytes = 2,147,483,712 bytes - large_string_size = 2 * GIB + 64 - large_string = "A" * large_string_size - - payload = { - "inputs": [ - { - "name": "INPUT0", - "datatype": "BYTES", - "shape": [1, 1], - "data": [large_string], - } - ] - } - - headers = {"Content-Type": "application/json"} - response = requests.post( - self._get_infer_url(model), headers=headers, json=payload - ) - - # Should fail with 400 bad request - self.assertEqual( - 400, - response.status_code, - "Expected error code for oversized JSON request, got: {}".format( - response.status_code - ), - ) - - # Verify error message - error_msg = response.content.decode() - self.assertIn( - "request JSON size of ", - error_msg, - ) - self.assertIn( - " bytes exceeds the maximum allowed input size of ", - error_msg, - ) - self.assertIn( - "Use --http-max-input-size to increase the limit.", - error_msg, - ) - - def _create_compressed_payload(self, target_size): - """Helper to create a gzip-compressed JSON payload of specified decompressed size.""" - shape_size = 1000 # Small actual data - payload = { - "inputs": [ - { - "name": "INPUT0", - "datatype": "FP32", - "shape": [1, shape_size], - "data": [1.0] * shape_size, - } - ] - } - json_str = json.dumps(payload, indent=4) - - # Pad with whitespace to reach target size (whitespace before closing brace is valid JSON) - padding_needed = target_size - len(json_str) - padded_json = json_str[:-1] + (" " * padding_needed) + json_str[-1] - - # Compress the payload - compressed_buffer = io.BytesIO() - with gzip.GzipFile(fileobj=compressed_buffer, mode="wb") as gz: - gz.write(padded_json.encode("utf-8")) - - return compressed_buffer.getvalue(), len(padded_json.encode("utf-8")) - - def test_default_limit_compressed(self): - """Test compressed inputs with default 64MB limit. - - This test verifies that the --http-max-input-size limit is enforced on - the decompressed data size, not just the compressed request size. - """ - model = "onnx_zero_1_float32" - - headers = { - "Content-Type": "application/json", - "Content-Encoding": "gzip", - } - - # Test case 1: Payload that decompresses to 64MB + 1MB (over limit) should fail - large_target_size = DEFAULT_LIMIT_BYTES + MIB - ( - large_compressed_data, - large_uncompressed_size, - ) = self._create_compressed_payload(large_target_size) - - # Verify uncompressed size is over 64MB limit - self.assertGreater( - large_uncompressed_size, - DEFAULT_LIMIT_BYTES, - f"Large payload should decompress to > 64MB, got {large_uncompressed_size}", - ) - - # Verify compressed size is under the limit - self.assertLess( - len(large_compressed_data), - DEFAULT_LIMIT_BYTES, - f"Compressed size should be under limit, got {len(large_compressed_data)}", - ) - - response = requests.post( - self._get_infer_url(model), data=large_compressed_data, headers=headers - ) - - # Should fail with 400 bad request - decompressed size exceeds limit - self.assertEqual( - 400, - response.status_code, - f"Expected 400 for compressed request that decompresses to >64MB, got: {response.status_code}", - ) - - # Verify error message contains size limit info - error_msg = response.content.decode() - self.assertIn( - "exceeds the maximum allowed value", - error_msg, - "Expected error message about exceeding max input size", - ) - - # Test case 2: Payload that decompresses to 64MB - 1MB (under limit) should succeed - small_target_size = DEFAULT_LIMIT_BYTES - MIB - ( - small_compressed_data, - small_uncompressed_size, - ) = self._create_compressed_payload(small_target_size) - - # Verify uncompressed size is under 64MB limit - self.assertLess( - small_uncompressed_size, - DEFAULT_LIMIT_BYTES, - f"Small payload should decompress to < 64MB, got {small_uncompressed_size}", - ) - - response = requests.post( - self._get_infer_url(model), data=small_compressed_data, headers=headers - ) - - # Should succeed with 200 OK - self.assertEqual( - 200, - response.status_code, - f"Expected 200 for compressed request within limit, got: {response.status_code}", - ) - - # Verify we got a valid response - result = response.json() - self.assertIn("outputs", result, "Response missing outputs field") - - def test_large_input_compressed(self): - """Test compressed inputs with custom 128MB limit set. - - This test verifies that compressed inputs work correctly when the - --http-max-input-size limit is increased. - """ - model = "onnx_zero_1_float32" - - headers = { - "Content-Type": "application/json", - "Content-Encoding": "gzip", - } - - # Test case 1: Input that decompresses to 128MB + 1MB (over limit) should fail - large_target_size = INCREASED_LIMIT_BYTES + MIB - ( - large_compressed_data, - large_uncompressed_size, - ) = self._create_compressed_payload(large_target_size) - - # Verify sizes - self.assertGreater( - large_uncompressed_size, - INCREASED_LIMIT_BYTES, - f"Large payload should decompress to > 128MB, got {large_uncompressed_size}", - ) - - response = requests.post( - self._get_infer_url(model), data=large_compressed_data, headers=headers - ) - - # Should fail with 400 bad request - self.assertEqual( - 400, - response.status_code, - f"Expected 400 for compressed request exceeding 128MB limit, got: {response.status_code}", - ) - - error_msg = response.content.decode() - self.assertIn( - "exceeds the maximum allowed value", - error_msg, - "Expected error message about exceeding max input size", - ) - - # Test case 2: Input that decompresses to 128MB - 1MB (under limit) should succeed - small_target_size = INCREASED_LIMIT_BYTES - MIB - ( - small_compressed_data, - small_uncompressed_size, - ) = self._create_compressed_payload(small_target_size) - - # Verify sizes - self.assertLess( - small_uncompressed_size, - INCREASED_LIMIT_BYTES, - f"Small payload should decompress to < 128MB, got {small_uncompressed_size}", - ) - self.assertGreater( - small_uncompressed_size, - DEFAULT_LIMIT_BYTES, - f"Small payload should decompress to > 64MB (default), got {small_uncompressed_size}", - ) - - response = requests.post( - self._get_infer_url(model), data=small_compressed_data, headers=headers - ) - - # Should succeed with 200 OK - self.assertEqual( - 200, - response.status_code, - f"Expected 200 for compressed request within 128MB limit, got: {response.status_code}", - ) - - # Verify we got a valid response - result = response.json() - self.assertIn("outputs", result, "Response missing outputs field") - - def test_no_leak_on_invalid_inference_header_length(self): - """ - Test that sending multiple malformed compressed requests does not cause memory growth on the server. - """ - leak_request_count = 100 - max_rss_growth_bytes = 32 * MIB - model = "onnx_zero_1_float32" - - body = gzip.compress(b" " * MIB) - headers = { - "Content-Type": "application/json", - "Content-Encoding": "gzip", - "Inference-Header-Content-Length": "9999999", - } - url = self._get_infer_url(model) - - server = get_server_process_from_env() - - with requests.Session() as session: - # Warm up the failure path so one-time allocations do not look - # like leaks. - resp = session.post(url, data=body, headers=headers) - self.assertEqual( - 400, - resp.status_code, - f"Expected status code 400, got {resp.status_code}: " - f"{resp.content[:200]!r}", - ) - - rss_before = server.memory_info().rss - for _ in range(leak_request_count): - resp = session.post(url, data=body, headers=headers) - self.assertEqual( - 400, - resp.status_code, - f"Expected status code 400, got {resp.status_code}: " - f"{resp.content[:200]!r}", - ) - rss_after = server.memory_info().rss - - growth = rss_after - rss_before - print( - f"RSS: before={rss_before / MIB:.1f} MIB, " - f"after={rss_after / MIB:.1f} MIB, " - f"growth={growth / MIB:.1f} MIB, " - f"limit={max_rss_growth_bytes / MIB:.0f} MIB", - flush=True, - ) - self.assertLess( - growth, - max_rss_growth_bytes, - f"Server RSS grew by {growth / MIB:.1f} MIB after " - f"{leak_request_count} malformed compressed requests " - f"(limit {max_rss_growth_bytes / MIB:.0f} MIB).", - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/qa/L0_http/http_request_many_chunks.py b/qa/L0_http/http_request_many_chunks.py deleted file mode 100755 index d733fb000c..0000000000 --- a/qa/L0_http/http_request_many_chunks.py +++ /dev/null @@ -1,178 +0,0 @@ -#!/usr/bin/python -# Copyright 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import socket -import sys -import unittest - -sys.path.append("../common") -from test_util import MIB, get_server_process_from_env, wait_for_stable_rss - - -class HTTPRequestManyChunksTest(unittest.TestCase): - def setUp(self): - self._local_host = "localhost" - self._http_port = 8000 - self._model_name = "simple" - # Must match server kMaxChunkedChunks (http_server.cc). - self._k_max_chunked_chunks = 65536 - self._over_max_chunks_error = f"Chunked request body exceeds maximum of {self._k_max_chunked_chunks} non-empty chunks. Send fewer or larger HTTP chunks." - - def _infer_chunked_header(self): - return ( - f"POST /v2/models/{self._model_name}/infer HTTP/1.1\r\n" - f"Inference-Header-Content-Length: 0\r\n" - ) - - def send_chunked_request( - self, - header: str, - chunk_count: int, - expected_response: str, - expected_http_status=400, - ): - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - header = ( - f"{header}" - f"Host: {self._local_host}:{self._http_port}\r\n" - f"Content-Type: application/octet-stream\r\n" - f"Transfer-Encoding: chunked\r\n" - f"Connection: close\r\n" - f"\r\n" - ) - try: - s.connect((self._local_host, self._http_port)) - # HTTP request with chunked encoding - s.sendall((header.encode())) - - # Send chunked payload - for _ in range(chunk_count): - try: - s.send(b"1\r\nA\r\n") - except (BrokenPipeError, ConnectionResetError): - break - try: - s.sendall(b"0\r\n\r\n") - except (BrokenPipeError, ConnectionResetError): - # Server may close/reset early after deciding on an error response. - # Ignore send failure here and continue reading any available response bytes. - pass - - response = b"" - while True: - try: - chunk = s.recv(4096) - if not chunk: - break - response += chunk - except ConnectionResetError: - break - except socket.timeout: - break - self.assertTrue( - response, - "expected error response body, but socket closed/reset before any bytes", - ) - status_line = response.split(b"\r\n", 1)[0].decode(errors="replace") - self.assertTrue( - status_line.startswith(f"HTTP/1.1 {expected_http_status} "), - f"expected HTTP status {expected_http_status}, got {status_line!r}", - ) - self.assertIn(expected_response, response.decode()) - except Exception as e: - raise (e) - finally: - s.close() - - def test_chunked_infer_at_max_chunks(self): - """Exactly kMaxChunkedChunks non-empty chunks: 400 request input error.""" - self.send_chunked_request( - self._infer_chunked_header(), - self._k_max_chunked_chunks, - "Raw request must only have 1 input (found 1) to be deduced but got 2 " - "inputs in 'simple' model configuration", - ) - - def test_chunked_infer_rejected_over_max_chunks(self): - """kMaxChunkedChunks+1 chunks: 400 request error with bounded RSS growth.""" - - # Warm up failure path to avoid one-time allocation noise in RSS checks. - self.send_chunked_request( - self._infer_chunked_header(), - self._k_max_chunked_chunks + 1, - self._over_max_chunks_error, - ) - - def test_chunked_infer_over_max_chunks_reject_with_bounded_rss_growth(self): - many_chunks = 1000000 - - # verify server is running - server = get_server_process_from_env("SERVER_PID") - self.assertTrue(server.is_running()) - - # warm up and wait until RSS is stable. - self.send_chunked_request( - self._infer_chunked_header(), - many_chunks, # way over max chunks - self._over_max_chunks_error, - ) - # Wait until RSS is stable across several measurements before continuing. - server = get_server_process_from_env("SERVER_PID") - wait_for_stable_rss(server) - - # Monitor RSS growth over 100 requests. - repeat_request_count = 100 - rss_before = server.memory_info().rss - max_rss_growth_bytes = 1 * MIB - - for _ in range(repeat_request_count): - self.send_chunked_request( - self._infer_chunked_header(), - many_chunks, # way over max chunks - self._over_max_chunks_error, - ) - - rss_after = server.memory_info().rss - growth = rss_after - rss_before - print( - f"RSS: before={rss_before / MIB:.1f} MiB, " - f"after={rss_after / MIB:.1f} MiB, " - f"growth={growth / MIB:.1f} MiB, " - f"limit={max_rss_growth_bytes / MIB:.0f} MiB", - flush=True, - ) - self.assertLess( - growth, - max_rss_growth_bytes, - f"Server RSS grew by {growth / MIB:.1f} MiB after " - f"{repeat_request_count} over-limit chunked infer requests " - f"(limit {max_rss_growth_bytes / MIB:.0f} MiB).", - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/qa/L0_http/http_test.py b/qa/L0_http/http_test.py index 9ea65fddf6..638ccbbbf8 100755 --- a/qa/L0_http/http_test.py +++ b/qa/L0_http/http_test.py @@ -1,5 +1,5 @@ #!/usr/bin/python -# Copyright 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -236,104 +236,6 @@ def test_descriptive_status_code(self): ) t.join() - def test_buffer_size_overflow(self): - model = "onnx_zero_1_float32" - - # Test for overflow within GetElementCount() - payload1 = { - "inputs": [ - { - "name": "INPUT0", - "shape": [ - 2**4, - 2**60 + 2, - ], # This evaluates to 2^64 + 32 during GetElementCount() - "datatype": "FP32", - "data": [1.0], - } - ] - } - - # Test for overflow with type_byte_size multiplication - payload2 = { - "inputs": [ - { - "name": "INPUT0", - "shape": [ - 2**2, - 2**60 + 2, - ], # This evaluates to 2^64 + 32 during type_byte_size multiplication since FP32 is 4 bytes - "datatype": "FP32", - "data": [1.0], - } - ] - } - - # Send request and expect a 400 error with specific overflow message - headers = {"Content-Type": "application/json"} - - # Test the first payload (GetElementCount overflow) - r1 = requests.post(self._get_infer_url(model), json=payload1, headers=headers) - - self.assertEqual( - 400, - r1.status_code, - "Expected error code 400 for GetElementCount overflow check; got: {}".format( - r1.status_code - ), - ) - - error_message1 = r1.content.decode() - self.assertIn( - "causes total element count to exceed maximum size of", error_message1 - ) - - # Test the second payload (type_byte_size multiplication overflow) - r2 = requests.post(self._get_infer_url(model), json=payload2, headers=headers) - - self.assertEqual( - 400, - r2.status_code, - "Expected error code 400 for type_byte_size multiplication overflow check; got: {}".format( - r2.status_code - ), - ) - - error_message2 = r2.content.decode() - self.assertIn("byte size overflow for input", error_message2) - - def test_negative_dimensions(self): - model = "onnx_zero_1_float32" - - payload = { - "inputs": [ - { - "name": "INPUT0", - "shape": [2, -5], # Negative dimension should be invalid - "datatype": "FP32", - "data": [1.0], - } - ] - } - - # Send request and expect a 500 error - headers = {"Content-Type": "application/json"} - r = requests.post(self._get_infer_url(model), json=payload, headers=headers) - - self.assertEqual( - 500, - r.status_code, - "Expected error code 500 for negative dimension; got: {}".format( - r.status_code - ), - ) - - error_message = r.content.decode() - self.assertIn( - "Unable to parse 'shape': attempt to access JSON non-unsigned-integer as unsigned-integer", - error_message, - ) - def test_loading_large_invalid_model(self): # Generate large base64 encoded data data_length = 1 << 31 @@ -364,173 +266,13 @@ def test_loading_large_invalid_model(self): try: error_message = response.json().get("error", "") self.assertIn( - "request JSON size", - error_message, - ) - self.assertIn( - " exceeds the maximum allowed input size", + "'file:1/model.onnx' exceeds the maximum allowed data size limit " + "INT_MAX", error_message, ) except ValueError: self.fail("Response is not valid JSON") - def test_load_oversized_file_parameter(self): - # Single path component longer than NAME_MAX (255 on typical Linux) must - # be rejected without terminating the server (filesystem_error handled). - long_path = "file:" + ("A" * 256) - payload = { - "parameters": { - long_path: "YQ==", - "config": "{}", - } - } - headers = {"Content-Type": "application/json"} - response = requests.post( - self._get_load_model_url("onnx_zero_1_float32"), - headers=headers, - json=payload, - ) - # TODO: [TRI-958] Status code 400 is more appropriate here - self.assertEqual( - 500, - response.status_code, - "Expected 500 for oversized file parameter; got {}".format( - response.status_code - ), - ) - try: - self.assertIn( - "failed to poll from model repository", response.json().get("error", "") - ) - except ValueError: - self.fail("Response is not valid JSON") - health = requests.get("http://localhost:8000/v2/health/ready", timeout=10) - self.assertEqual( - 200, - health.status_code, - "server must stay up after rejected load", - ) - - def test_json_recursion_depth_limit(self): - """Test that server properly handles and rejects deeply nested JSON.""" - - def create_nested_json(depth, value): - for _ in range(depth): - value = f"[{value}]" - return json.loads(value) - - headers = {"Content-Type": "application/json"} - test_matrix = [ - # (datatype, data, model, json_depth, should_succeed) - ("BYTES", '"hello"', "simple_identity", 120, False), - ("BYTES", '"hello"', "simple_identity", 50, True), - ("INT64", "123", "simple_identity_int64", 120, False), - ("INT64", "123", "simple_identity_int64", 50, True), - ] - - for dtype, data, model, json_depth, should_succeed in test_matrix: - with self.subTest( - datatype=dtype, depth=json_depth, should_succeed=should_succeed - ): - payload = { - "inputs": [ - { - "name": "INPUT0", - "datatype": dtype, - "shape": [1, 1], - "data": create_nested_json(json_depth, data), - } - ] - } - - response = requests.post( - self._get_infer_url(model), headers=headers, json=payload - ) - - if should_succeed: - self.assertEqual(response.status_code, 200) - else: - self.assertNotEqual(response.status_code, 200) - try: - error_message = response.json().get("error", "") - self.assertIn( - "JSON nesting depth exceeds maximum allowed limit (100)", - error_message, - ) - except ValueError: - self.fail("Response is not valid JSON") - - def test_duplicate_output_names(self): - """Test that duplicate output names are rejected""" - model = "onnx_zero_1_float32" - input_data = np.arange(8, dtype=np.float32).flatten().tolist() - - num_duplicates = 2 - payload = { - "inputs": [ - { - "name": "INPUT0", - "datatype": "FP32", - "shape": [1, 8], - "data": [input_data], - } - ], - "outputs": [{"name": "OUTPUT0"} for _ in range(num_duplicates)], - } - - headers = {"Content-Type": "application/json"} - r = requests.post(self._get_infer_url(model), json=payload, headers=headers) - self.assertEqual( - 400, - r.status_code, - "Expected error code 400 for duplicate output names; got: {}".format( - r.status_code - ), - ) - error_message = r.json().get("error", "") - self.assertIn("output 'OUTPUT0' already exists in request", error_message) - - # Verify server is still healthy after the bad request - health_url = "http://localhost:8000/v2/health/live" - health_r = requests.get(health_url) - self.assertEqual( - 200, - health_r.status_code, - "Server is not healthy after duplicate output request", - ) - - def test_repository_index_deeply_nested_json(self): - """Test for deeply nested JSON on model repository index.""" - depth = 250000 - nested = ("[" * depth) + "true" + ("]" * depth) - payload = '{"ready":' + nested + "}" - - # Keep request below default --http-max-input-size so parsing path is exercised. - self.assertLess(len(payload), 64 * 1024 * 1024) - - response = requests.post( - "http://localhost:8000/v2/repository/index", - data=payload, - headers={"Content-Type": "application/json"}, - timeout=60, - ) - self.assertEqual( - 400, - response.status_code, - "Expected repository index request to fail on invalid 'ready' type.", - ) - self.assertIn( - "Invalid value for 'ready': expected a boolean", - response.json()["error"], - ) - - live_response = requests.get("http://localhost:8000/v2/health/live", timeout=10) - self.assertEqual( - 200, - live_response.status_code, - "Expected server to remain live after deeply nested JSON request.", - ) - if __name__ == "__main__": unittest.main() diff --git a/qa/L0_http/test.sh b/qa/L0_http/test.sh index e6b48272d2..c36024e007 100755 --- a/qa/L0_http/test.sh +++ b/qa/L0_http/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -40,7 +40,6 @@ fi export CUDA_VISIBLE_DEVICES=0 -source ../common/util.sh RET=0 CLIENT_PLUGIN_TEST="./http_client_plugin_test.py" @@ -129,7 +128,8 @@ rm -f *.log.* set -e CLIENT_LOG=`pwd`/client.log -SERVER_ARGS="--backend-directory=${BACKEND_DIR} --model-repository=${MODELDIR} --allow-client-shm=true" +SERVER_ARGS="--backend-directory=${BACKEND_DIR} --model-repository=${MODELDIR}" +source ../common/util.sh run_server if [ "$SERVER_PID" == "0" ]; then @@ -163,13 +163,13 @@ for i in \ BASE=$(basename -- $i) SUFFIX="${BASE%.*}" if [ $SUFFIX == "image_client" ]; then - python $i -m densenet_onnx -s INCEPTION -a -c 1 -b 1 $IMAGE >> "${CLIENT_LOG}.async.${SUFFIX}" 2>&1 + python $i -m inception_onnx -s INCEPTION -a -c 1 -b 1 $IMAGE >> "${CLIENT_LOG}.async.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.async.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.async.${SUFFIX} RET=1 fi - python $i -m densenet_onnx -s INCEPTION -c 1 -b 1 $IMAGE >> "${CLIENT_LOG}.${SUFFIX}" 2>&1 + python $i -m inception_onnx -s INCEPTION -c 1 -b 1 $IMAGE >> "${CLIENT_LOG}.${SUFFIX}" 2>&1 if [ `grep -c VULTURE ${CLIENT_LOG}.${SUFFIX}` != "1" ]; then echo -e "\n***\n*** Failed. Expected 1 VULTURE results\n***" cat $CLIENT_LOG.${SUFFIX} @@ -255,7 +255,7 @@ echo -n 'username:' > pswd echo "password" | openssl passwd -stdin -apr1 >> pswd nginx -c `pwd`/$NGINX_CONF -python $BASIC_AUTH_TEST >> ${CLIENT_LOG}.python.plugin.auth 2>&1 +python $BASIC_AUTH_TEST if [ $? -ne 0 ]; then cat ${CLIENT_LOG}.python.plugin.auth RET=1 @@ -380,15 +380,9 @@ if [ "$code" != "200" ]; then RET=1 fi if [ `grep -c "\[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32\]" ./curl.out` != "1" ]; then - echo -e "\[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32\] not found in output when expected" - cat ./curl.out - echo "" RET=1 fi if [ `grep -c "\[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0\]" ./curl.out` != "1" ]; then - echo -e "\[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0\] not found in output when expected" - cat ./curl.out - echo "" RET=1 fi @@ -403,15 +397,9 @@ if [ "$code" != "200" ]; then RET=1 fi if [ `grep -c "\[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32\]" ./curl.out` != "0" ]; then - echo -e "\[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32\] found in output when not expected" - cat ./curl.out - echo "" RET=1 fi if [ `grep -c "\[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0\]" ./curl.out` != "1" ]; then - echo -e "\[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0\] not found in output when expected" - cat ./curl.out - echo "" RET=1 fi @@ -426,15 +414,9 @@ if [ "$code" != "200" ]; then RET=1 fi if [ `grep -c "\[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32\]" ./curl.out` != "0" ]; then - echo -e "\[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32\] found in output when not expected" - cat ./curl.out - echo "" RET=1 fi if [ `grep -c "\[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0\]" ./curl.out` != "1" ]; then - echo -e "\[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0\] not found in output when expected" - cat ./curl.out - echo "" RET=1 fi @@ -449,15 +431,9 @@ if [ "$code" != "200" ]; then RET=1 fi if [ `grep -c "\[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32\]" ./curl.out` != "0" ]; then - echo -e "\[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32\] found in output when not expected" - cat ./curl.out - echo "" RET=1 fi if [ `grep -c "\[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0\]" ./curl.out` != "0" ]; then - echo -e "\[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0\] found in output when not expected" - cat ./curl.out - echo "" RET=1 fi @@ -473,15 +449,9 @@ if [ "$code" != "200" ]; then RET=1 fi if [ `grep -c "\[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32\]" ./curl.out` != "1" ]; then - echo -e "\[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32\] not found in output when expected" - cat ./curl.out - echo "" RET=1 fi if [ `grep -c "\[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0\]" ./curl.out` != "1" ]; then - echo -e "\[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0\] not found in output when expected" - cat ./curl.out - echo "" RET=1 fi @@ -495,10 +465,7 @@ if [ "$code" == "200" ]; then echo -e "\n***\n*** Test Failed\n***" RET=1 fi -if [ `grep -c "\{\"error\":\"Failed to parse 'data' field: shape does not match true shape\"\}" ./curl.out` != "1" ]; then - echo -e "\{\"error\":\"Failed to parse 'data' field: shape does not match true shape\"\} not found in output when expected" - cat ./curl.out - echo "" +if [ `grep -c "\{\"error\":\"Unable to parse 'data': Shape does not match true shape of 'data' field\"\}" ./curl.out` != "1" ]; then RET=1 fi @@ -512,9 +479,6 @@ if [ "$code" == "200" ]; then RET=1 fi if [ `grep -c "\{\"error\":\"Unable to parse 'data': Shape does not match true shape of 'data' field\"\}" ./curl.out` != "1" ]; then - echo -e "\{\"error\":\"Unable to parse 'data': Shape does not match true shape of 'data' field\"\} not found in output when expected" - cat ./curl.out - echo "" RET=1 fi @@ -529,15 +493,9 @@ if [ "$code" != "200" ]; then RET=1 fi if [ `grep -c "\[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32\]" ./curl.out` != "1" ]; then - echo -e "\[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32\] not found in output when expected" - cat ./curl.out - echo "" RET=1 fi if [ `grep -c "\[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0\]" ./curl.out` != "1" ]; then - echo -e "\[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0\] not found in output when expected" - cat ./curl.out - echo "" RET=1 fi @@ -612,7 +570,7 @@ done # Run python http aio unit test PYTHON_HTTP_AIO_TEST=python_http_aio_test.py CLIENT_LOG=`pwd`/python_http_aio_test.log -SERVER_ARGS="--backend-directory=${BACKEND_DIR} --model-repository=${MODELDIR} --allow-client-shm=true" +SERVER_ARGS="--backend-directory=${BACKEND_DIR} --model-repository=${MODELDIR}" run_server if [ "$SERVER_PID" == "0" ]; then echo -e "\n***\n*** Failed to start $SERVER\n***" @@ -654,13 +612,7 @@ cp -r ${MODELDIR}/onnx_zero_1_float32 ${MODELDIR}/onnx_zero_1_float32_queue && \ echo " }" >> config.pbtxt && \ echo "}" >> config.pbtxt) -cp -r ./models/simple_identity ${MODELDIR} -cp -r ./models/simple_identity ${MODELDIR}/simple_identity_int64 && \ - (cd $MODELDIR/simple_identity_int64 && \ - sed -i "s/TYPE_STRING/TYPE_INT64/" config.pbtxt && \ - sed -i "s/simple_identity/simple_identity_int64/" config.pbtxt) - -SERVER_ARGS="--backend-directory=${BACKEND_DIR} --model-repository=${MODELDIR} --model-control-mode=explicit --load-model=*" +SERVER_ARGS="--backend-directory=${BACKEND_DIR} --model-repository=${MODELDIR}" SERVER_LOG="./inference_server_http_test.log" CLIENT_LOG="./http_test.log" run_server @@ -672,7 +624,7 @@ fi TEST_RESULT_FILE='test_results.txt' PYTHON_TEST=http_test.py -EXPECTED_NUM_TESTS=16 +EXPECTED_NUM_TESTS=10 set +e python $PYTHON_TEST >$CLIENT_LOG 2>&1 if [ $? -ne 0 ]; then @@ -695,9 +647,9 @@ wait $SERVER_PID # Helper library to parse SSE events # https://github.com/mpetazzoni/sseclient -pip install sseclient-py psutil +pip install sseclient-py -SERVER_ARGS="--model-repository=`pwd`/../python_models/generate_models --log-verbose=1" +SERVER_ARGS="--model-repository=`pwd`/../python_models/generate_models" SERVER_LOG="./inference_server_generate_endpoint_test.log" CLIENT_LOG="./generate_endpoint_test.log" run_server @@ -710,9 +662,9 @@ fi ## Python Unit Tests TEST_RESULT_FILE='test_results.txt' PYTHON_TEST=generate_endpoint_test.py -EXPECTED_NUM_TESTS=18 +EXPECTED_NUM_TESTS=17 set +e -python $PYTHON_TEST > $CLIENT_LOG 2>&1 +python $PYTHON_TEST >$CLIENT_LOG 2>&1 if [ $? -ne 0 ]; then cat $CLIENT_LOG RET=1 @@ -795,172 +747,7 @@ set -e kill $SERVER_PID wait $SERVER_PID -### Test HTTP input size limits ### - -# Setup models needed for the test -MODELDIR=http_input_size_limit_test_models -mkdir -p $MODELDIR -rm -rf ${MODELDIR}/* -cp -r $DATADIR/qa_identity_model_repository/onnx_zero_1_float32 ${MODELDIR}/. -cp -r ./models/simple_identity ${MODELDIR}/. - -# First run with default size limit - large inputs should fail -SERVER_ARGS="--model-repository=${MODELDIR}" -SERVER_LOG="./inference_server_default_limit.log" -CLIENT_LOG="./http_input_size_limit_default.log" -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 -fi - -set +e -# Run test to verify that large inputs fail with default limit -python http_input_size_limit_test.py InferSizeLimitTest.test_default_limit_raw_binary >> $CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Default Input Size Limit Test Failed for raw binary input\n***" - RET=1 -fi - -python http_input_size_limit_test.py InferSizeLimitTest.test_default_limit_json >> $CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Default Input Size Limit Test Failed for JSON input\n***" - RET=1 -fi - -python http_input_size_limit_test.py InferSizeLimitTest.test_large_string_in_json >> $CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Default Input Size Limit Test Failed for large string in JSON\n***" - RET=1 -fi - -python http_input_size_limit_test.py InferSizeLimitTest.test_default_limit_compressed >> $CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Default Input Size Limit Test Failed for compressed input\n***" - RET=1 -fi - -# Run test to verify that large inputs fail with default limit -python http_input_size_limit_test.py InferSizeLimitTest.test_json_dtype_size_expansion_exceeds_limit_error >> $CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Default Input Size Limit Test Failed for type size explosion\n***" - RET=1 -fi - -# Test that sending multiple malformed compressed requests does not cause memory leaks on the server. -SERVER_PID=$SERVER_PID python http_input_size_limit_test.py InferSizeLimitTest.test_no_leak_on_invalid_inference_header_length >> $CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Decompression Leak Regression Failed\n***\n***" - RET=1 -fi -set -e - -kill $SERVER_PID -wait $SERVER_PID - -# Now run with increased size limit (128MB) - large inputs should succeed -SERVER_ARGS="--model-repository=${MODELDIR} --http-max-input-size=$((2**27))" -SERVER_LOG="./inference_server_increased_limit.log" -CLIENT_LOG="./http_input_size_limit_increased.log" -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER with increased HTTP input size limit\n***" - cat $SERVER_LOG - exit 1 -fi - -rm -f $CLIENT_LOG -set +e -python http_input_size_limit_test.py InferSizeLimitTest.test_large_input_raw_binary >> $CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Input Size Limit Test Failed for raw binary input with increased limits\n***" - RET=1 -fi - -python http_input_size_limit_test.py InferSizeLimitTest.test_large_input_json >> $CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Input Size Limit Test Failed for JSON input with increased limits\n***" - RET=1 -fi - -python http_input_size_limit_test.py InferSizeLimitTest.test_large_input_compressed >> $CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Input Size Limit Test Failed for compressed input with increased limits\n***" - RET=1 -fi -set -e - -kill $SERVER_PID -wait $SERVER_PID - -# Test with zero max input size - should fail to start -SERVER_ARGS="--model-repository=${MODELDIR} --http-max-input-size=0" -SERVER_LOG="./inference_server_zero_limit.log" -CLIENT_LOG="./http_input_size_limit_zero.log" -run_server -if [ "$SERVER_PID" != "0" ]; then - echo -e "\n***\n*** Server should not start with zero max input size\n***" - kill $SERVER_PID - wait $SERVER_PID - RET=1 -elif [ `grep -c "Error: --http-max-input-size must be greater than 0." ${SERVER_LOG}` != "1" ]; then - echo -e "\n***\n*** Failed. Expected '--http-max-input-size must be greater than 0' to be found in log\n***" - cat $SERVER_LOG - RET=1 -fi - -# Test with negative max input size - should fail to start -SERVER_ARGS="--model-repository=${MODELDIR} --http-max-input-size=-1024" -SERVER_LOG="./inference_server_negative_limit.log" -CLIENT_LOG="./http_input_size_limit_negative.log" -run_server -if [ "$SERVER_PID" != "0" ]; then - echo -e "\n***\n*** Server should not start with negative max input size\n***" - kill $SERVER_PID - wait $SERVER_PID - RET=1 -elif [ `grep -c "Error: --http-max-input-size must be greater than 0." ${SERVER_LOG}` != "1" ]; then - echo -e "\n***\n*** Failed. Expected '--http-max-input-size must be greater than 0' to be found in log\n***" - cat $SERVER_LOG - RET=1 -fi - -### Test HTTP Requests Containing Many Chunks ### -MODELDIR="`pwd`/models" -REQUEST_MANY_CHUNKS_PY="http_request_many_chunks.py" -CLIENT_LOG="./client.http_request_many_chunks.log" -SERVER_ARGS="--model-repository=${MODELDIR} --allow-client-shm=true --log-verbose=1 --model-control-mode=explicit --load-model=simple" -SERVER_LOG="./inference_server_request_many_chunks.log" - -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 -fi - -set +e -SERVER_PID=$SERVER_PID python $REQUEST_MANY_CHUNKS_PY -v >> ${CLIENT_LOG} 2>&1 -if [ $? -ne 0 ]; then - echo -e "\n***\n*** HTTP Request Many Chunks Test Failed\n***" - cat $SERVER_LOG - cat $CLIENT_LOG - RET=1 -fi -set -e - -kill $SERVER_PID -wait $SERVER_PID +### if [ $RET -eq 0 ]; then echo -e "\n***\n*** Test Passed\n***" diff --git a/qa/L0_http_fuzz/test.sh b/qa/L0_http_fuzz/test.sh index 96433aaed9..1abe08b487 100755 --- a/qa/L0_http_fuzz/test.sh +++ b/qa/L0_http_fuzz/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2020-2026, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2020-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -65,7 +65,7 @@ function_install_python38() { # Install test script dependencies pip3 install --upgrade wheel setuptools boofuzz==0.3.0 "numpy<2" pillow attrdict future grpcio requests gsutil \ - awscli six grpcio-channelz prettytable virtualenv ml_dtypes + awscli six grpcio-channelz prettytable virtualenv } function_install_python38 diff --git a/qa/L0_infer/infer_test.py b/qa/L0_infer/infer_test.py index a195092c1b..8f16a7fe10 100755 --- a/qa/L0_infer/infer_test.py +++ b/qa/L0_infer/infer_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -42,7 +42,6 @@ TEST_CUDA_SHARED_MEMORY = bool(int(os.environ.get("TEST_CUDA_SHARED_MEMORY", 0))) CPU_ONLY = os.environ.get("TRITON_SERVER_CPU_ONLY") is not None TEST_VALGRIND = bool(int(os.environ.get("TEST_VALGRIND", 0))) -VALGRIND_TESTS = bool(int(os.environ.get("VALGRIND_TESTS", 0))) USE_GRPC = os.environ.get("USE_GRPC", 1) != "0" USE_HTTP = os.environ.get("USE_HTTP", 1) != "0" @@ -70,6 +69,7 @@ def _full_exact( output0_raw, output1_raw, swap, + network_timeout=NETWORK_TIMEOUT, ): def _infer_exact_helper( tester, @@ -157,13 +157,44 @@ def _infer_exact_helper( input_dtype, output0_dtype, output1_dtype, + (input_size,), + (input_size,), + (input_size,), ): ensemble_prefix.append(prefix) + if tu.validate_for_onnx_model( + input_dtype, + output0_dtype, + output1_dtype, + (input_size,), + (input_size,), + (input_size,), + ): + for prefix in ensemble_prefix: + for pf in ["onnx"]: + if pf in BACKENDS: + _infer_exact_helper( + self, + prefix + pf, + (input_size,), + 8, + input_dtype, + output0_dtype, + output1_dtype, + output0_raw=output0_raw, + output1_raw=output1_raw, + swap=swap, + network_timeout=network_timeout, + ) + if not CPU_ONLY and tu.validate_for_trt_model( input_dtype, output0_dtype, output1_dtype, + (input_size, 1, 1), + (input_size, 1, 1), + (input_size, 1, 1), ): for prefix in ensemble_prefix: if "plan" in BACKENDS: @@ -198,6 +229,9 @@ def _infer_exact_helper( input_dtype, output0_dtype, output1_dtype, + (input_size,), + (input_size,), + (input_size,), ): for prefix in ensemble_prefix: if "onnx" in BACKENDS: @@ -632,59 +666,16 @@ def test_mix_iff(self): swap=False, ) - if not VALGRIND_TESTS: - - def test_raw_version_latest_1(self): - input_size = 16 - tensor_shape = (1, input_size) - - # There are 3 versions of onnx_int8_int8_int8 but - # only version 3 should be available - for platform in ["onnx"]: - if platform not in BACKENDS: - continue - try: - iu.infer_exact( - self, - platform, - tensor_shape, - 1, - np.int8, - np.int8, - np.int8, - model_version=1, - swap=False, - use_http=USE_HTTP, - use_grpc=USE_GRPC, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) - except InferenceServerException as ex: - self.assertTrue( - ex.message().startswith("Request for unknown model") - ) - - try: - iu.infer_exact( - self, - platform, - tensor_shape, - 1, - np.int8, - np.int8, - np.int8, - model_version=2, - swap=True, - use_http=USE_HTTP, - use_grpc=USE_GRPC, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) - except InferenceServerException as ex: - self.assertTrue( - ex.message().startswith("Request for unknown model") - ) + def test_raw_version_latest_1(self): + input_size = 16 + tensor_shape = (1, input_size) + # There are 3 versions of onnx_int8_int8_int8 but + # only version 3 should be available + for platform in ["onnx"]: + if platform not in BACKENDS: + continue + try: iu.infer_exact( self, platform, @@ -693,52 +684,25 @@ def test_raw_version_latest_1(self): np.int8, np.int8, np.int8, - model_version=3, - swap=True, + model_version=1, + swap=False, use_http=USE_HTTP, use_grpc=USE_GRPC, use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) + except InferenceServerException as ex: + self.assertTrue(ex.message().startswith("Request for unknown model")) - def test_raw_version_latest_2(self): - input_size = 16 - tensor_shape = (1, input_size) - - # There are 3 versions of onnx_int16_int16_int16 but only - # versions 2 and 3 should be available - for platform in ["onnx"]: - if platform not in BACKENDS: - continue - try: - iu.infer_exact( - self, - platform, - tensor_shape, - 1, - np.int16, - np.int16, - np.int16, - model_version=1, - swap=False, - use_http=USE_HTTP, - use_grpc=USE_GRPC, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) - except InferenceServerException as ex: - self.assertTrue( - ex.message().startswith("Request for unknown model") - ) - + try: iu.infer_exact( self, platform, tensor_shape, 1, - np.int16, - np.int16, - np.int16, + np.int8, + np.int8, + np.int8, model_version=2, swap=True, use_http=USE_HTTP, @@ -746,6 +710,35 @@ def test_raw_version_latest_2(self): use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) + except InferenceServerException as ex: + self.assertTrue(ex.message().startswith("Request for unknown model")) + + iu.infer_exact( + self, + platform, + tensor_shape, + 1, + np.int8, + np.int8, + np.int8, + model_version=3, + swap=True, + use_http=USE_HTTP, + use_grpc=USE_GRPC, + use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, + use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, + ) + + def test_raw_version_latest_2(self): + input_size = 16 + tensor_shape = (1, input_size) + + # There are 3 versions of onnx_int16_int16_int16 but only + # versions 2 and 3 should be available + for platform in ["onnx"]: + if platform not in BACKENDS: + continue + try: iu.infer_exact( self, platform, @@ -754,31 +747,6 @@ def test_raw_version_latest_2(self): np.int16, np.int16, np.int16, - model_version=3, - swap=True, - use_http=USE_HTTP, - use_grpc=USE_GRPC, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) - - def test_raw_version_all(self): - input_size = 16 - tensor_shape = (1, input_size) - - # There are 3 versions of onnx_int32_int32_int32 and all should - # be available. - for platform in ["onnx"]: - if platform not in BACKENDS: - continue - iu.infer_exact( - self, - platform, - tensor_shape, - 1, - np.int32, - np.int32, - np.int32, model_version=1, swap=False, use_http=USE_HTTP, @@ -786,14 +754,129 @@ def test_raw_version_all(self): use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) + except InferenceServerException as ex: + self.assertTrue(ex.message().startswith("Request for unknown model")) + + iu.infer_exact( + self, + platform, + tensor_shape, + 1, + np.int16, + np.int16, + np.int16, + model_version=2, + swap=True, + use_http=USE_HTTP, + use_grpc=USE_GRPC, + use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, + use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, + ) + iu.infer_exact( + self, + platform, + tensor_shape, + 1, + np.int16, + np.int16, + np.int16, + model_version=3, + swap=True, + use_http=USE_HTTP, + use_grpc=USE_GRPC, + use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, + use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, + ) + + def test_raw_version_all(self): + input_size = 16 + tensor_shape = (1, input_size) + + # There are 3 versions of *_int32_int32_int32 and all should + # be available. + for platform in ["onnx"]: + if platform not in BACKENDS: + continue + iu.infer_exact( + self, + platform, + tensor_shape, + 1, + np.int32, + np.int32, + np.int32, + model_version=1, + swap=False, + use_http=USE_HTTP, + use_grpc=USE_GRPC, + use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, + use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, + ) + iu.infer_exact( + self, + platform, + tensor_shape, + 1, + np.int32, + np.int32, + np.int32, + model_version=2, + swap=True, + use_http=USE_HTTP, + use_grpc=USE_GRPC, + use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, + use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, + ) + iu.infer_exact( + self, + platform, + tensor_shape, + 1, + np.int32, + np.int32, + np.int32, + model_version=3, + swap=True, + use_http=USE_HTTP, + use_grpc=USE_GRPC, + use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, + use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, + ) + + def test_raw_version_specific_1(self): + input_size = 16 + tensor_shape = (1, input_size) + + # There are 3 versions of *_float16_float16_float16 but only + # version 1 should be available. + for platform in ["onnx"]: + if platform not in BACKENDS: + continue + iu.infer_exact( + self, + platform, + tensor_shape, + 1, + np.float16, + np.float16, + np.float16, + model_version=1, + swap=False, + use_http=USE_HTTP, + use_grpc=USE_GRPC, + use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, + use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, + ) + + try: iu.infer_exact( self, platform, tensor_shape, 1, - np.int32, - np.int32, - np.int32, + np.float16, + np.float16, + np.float16, model_version=2, swap=True, use_http=USE_HTTP, @@ -801,14 +884,18 @@ def test_raw_version_all(self): use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) + except InferenceServerException as ex: + self.assertTrue(ex.message().startswith("Request for unknown model")) + + try: iu.infer_exact( self, platform, tensor_shape, 1, - np.int32, - np.int32, - np.int32, + np.float16, + np.float16, + np.float16, model_version=3, swap=True, use_http=USE_HTTP, @@ -816,149 +903,143 @@ def test_raw_version_all(self): use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) + except InferenceServerException as ex: + self.assertTrue(ex.message().startswith("Request for unknown model")) - def test_raw_version_specific_1(self): - input_size = 16 + def test_raw_version_specific_1_3(self): + input_size = 16 + + # There are 3 versions of *_float32_float32_float32 but only + # versions 1 and 3 should be available. + for platform in ("onnx", "plan"): + if platform == "plan" and CPU_ONLY: + continue + if platform not in BACKENDS: + continue tensor_shape = (1, input_size) + iu.infer_exact( + self, + platform, + tensor_shape, + 1, + np.float32, + np.float32, + np.float32, + model_version=1, + swap=False, + use_http=USE_HTTP, + use_grpc=USE_GRPC, + use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, + use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, + ) - # There are 3 versions of onnx_float16_float16_float16 but only - # version 1 should be available. - for platform in ["onnx"]: - if platform not in BACKENDS: - continue + try: iu.infer_exact( self, platform, tensor_shape, 1, - np.float16, - np.float16, - np.float16, - model_version=1, - swap=False, + np.float32, + np.float32, + np.float32, + model_version=2, + swap=True, use_http=USE_HTTP, use_grpc=USE_GRPC, use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) + except InferenceServerException as ex: + self.assertTrue(ex.message().startswith("Request for unknown model")) + + iu.infer_exact( + self, + platform, + tensor_shape, + 1, + np.float32, + np.float32, + np.float32, + model_version=3, + swap=True, + use_http=USE_HTTP, + use_grpc=USE_GRPC, + use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, + use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, + ) - try: + if ENSEMBLES: + if all(x in BACKENDS for x in ["onnx", "plan"]): + + def test_ensemble_mix_platform(self): + # Skip on CPU only machine as TensorRT model is used in this ensemble + if CPU_ONLY: + return + for bs in (1, 8): iu.infer_exact( self, - platform, - tensor_shape, - 1, - np.float16, - np.float16, - np.float16, - model_version=2, - swap=True, + "mix_platform", + (bs, 16), + bs, + np.float32, + np.float32, + np.float32, use_http=USE_HTTP, use_grpc=USE_GRPC, use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) - except InferenceServerException as ex: - self.assertTrue( - ex.message().startswith("Request for unknown model") - ) - try: + if "onnx" in BACKENDS: + + def test_ensemble_mix_type(self): + for bs in (1, 8): iu.infer_exact( self, - platform, - tensor_shape, - 1, - np.float16, - np.float16, - np.float16, - model_version=3, - swap=True, + "mix_type", + (bs, 16), + bs, + np.int32, + np.float32, + np.float32, use_http=USE_HTTP, use_grpc=USE_GRPC, use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) - except InferenceServerException as ex: - self.assertTrue( - ex.message().startswith("Request for unknown model") - ) - def test_raw_version_specific_1_3(self): - input_size = 16 - - # There are 3 versions of *_float32_float32_float32 but only - # versions 1 and 3 should be available. - for platform in ("onnx", "plan"): - if platform == "plan" and CPU_ONLY: - continue - if platform not in BACKENDS: - continue - tensor_shape = (1, input_size) - iu.infer_exact( - self, - platform, - tensor_shape, - 1, - np.float32, - np.float32, - np.float32, - model_version=1, - swap=False, - use_http=USE_HTTP, - use_grpc=USE_GRPC, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) + if all(x in BACKENDS for x in ["onnx", "plan"]): - try: + def test_ensemble_mix_ensemble(self): + for bs in (1, 8): iu.infer_exact( self, - platform, - tensor_shape, - 1, - np.float32, + "mix_ensemble", + (bs, 16), + bs, + np.int32, np.float32, np.float32, - model_version=2, - swap=True, use_http=USE_HTTP, use_grpc=USE_GRPC, use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) - except InferenceServerException as ex: - self.assertTrue( - ex.message().startswith("Request for unknown model") - ) - - iu.infer_exact( - self, - platform, - tensor_shape, - 1, - np.float32, - np.float32, - np.float32, - model_version=3, - swap=True, - use_http=USE_HTTP, - use_grpc=USE_GRPC, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) - if ENSEMBLES: - if all(x in BACKENDS for x in ["onnx", "libtorch"]): + if all( + x in BACKENDS + for x in [ + "onnx", + ] + ): - def test_ensemble_mix_platform(self): - # Skip on CPU only machine as TensorRT model is used in this ensemble - if CPU_ONLY: - return + def test_ensemble_mix_batch_nobatch(self): + base_names = ["batch_to_nobatch", "nobatch_to_batch"] + for name in base_names: for bs in (1, 8): iu.infer_exact( self, - "mix_platform", + name, (bs, 16), bs, np.float32, @@ -969,28 +1050,60 @@ def test_ensemble_mix_platform(self): use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) + iu.infer_exact( + self, + name + "_nobatch", + (8, 16), + 1, + np.float32, + np.float32, + np.float32, + use_http=USE_HTTP, + use_grpc=USE_GRPC, + use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, + use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, + ) + + # batch -> nobatch -> batch + for bs in (1, 8): + iu.infer_exact( + self, + "mix_nobatch_batch", + (bs, 16), + bs, + np.float32, + np.float32, + np.float32, + use_http=USE_HTTP, + use_grpc=USE_GRPC, + use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, + use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, + ) - if "onnx" in BACKENDS: + if not (TEST_SYSTEM_SHARED_MEMORY or TEST_CUDA_SHARED_MEMORY): - def test_ensemble_mix_type(self): + def test_ensemble_label_lookup(self): + if all(x in BACKENDS for x in ["onnx", "plan"]): + # Ensemble needs to look up label from the actual model for bs in (1, 8): iu.infer_exact( self, - "mix_type", + "mix_platform", (bs, 16), bs, - np.int32, np.float32, np.float32, + np.float32, + output0_raw=False, + output1_raw=False, use_http=USE_HTTP, use_grpc=USE_GRPC, use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) - if all(x in BACKENDS for x in ["onnx", "libtorch"]): - - def test_ensemble_mix_ensemble(self): + if all(x in BACKENDS for x in ["onnx", "plan"]): + # Label from the actual model will be passed along the nested ensemble for bs in (1, 8): iu.infer_exact( self, @@ -1000,148 +1113,55 @@ def test_ensemble_mix_ensemble(self): np.int32, np.float32, np.float32, + output0_raw=False, + output1_raw=False, use_http=USE_HTTP, use_grpc=USE_GRPC, use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) - if all( - x in BACKENDS - for x in [ - "onnx", - ] - ): - - def test_ensemble_mix_batch_nobatch(self): - base_names = ["batch_to_nobatch", "nobatch_to_batch"] - for name in base_names: - for bs in (1, 8): - iu.infer_exact( - self, - name, - (bs, 16), - bs, - np.float32, - np.float32, - np.float32, - use_http=USE_HTTP, - use_grpc=USE_GRPC, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) + if "onnx" in BACKENDS: + # If label file is provided, it will use the provided label file directly + try: iu.infer_exact( self, - name + "_nobatch", - (8, 16), + "wrong_label", + (1, 16), 1, + np.int32, np.float32, np.float32, - np.float32, + output0_raw=False, + output1_raw=False, use_http=USE_HTTP, use_grpc=USE_GRPC, use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) + except AssertionError: + # Sanity check that infer_exact failed since this ensemble is provided + # with unexpected labels + pass - # batch -> nobatch -> batch + if "onnx" in BACKENDS: for bs in (1, 8): iu.infer_exact( self, - "mix_nobatch_batch", + "label_override", (bs, 16), bs, + np.int32, np.float32, np.float32, - np.float32, + output0_raw=False, + output1_raw=False, use_http=USE_HTTP, use_grpc=USE_GRPC, use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, ) - if not (TEST_SYSTEM_SHARED_MEMORY or TEST_CUDA_SHARED_MEMORY): - - def test_ensemble_label_lookup(self): - if all(x in BACKENDS for x in ["onnx", "libtorch"]): - # Ensemble needs to look up label from the actual model - for bs in (1, 8): - iu.infer_exact( - self, - "mix_platform", - (bs, 16), - bs, - np.float32, - np.float32, - np.float32, - output0_raw=False, - output1_raw=False, - use_http=USE_HTTP, - use_grpc=USE_GRPC, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) - - if all(x in BACKENDS for x in ["onnx", "libtorch"]): - # Label from the actual model will be passed along the nested ensemble - for bs in (1, 8): - iu.infer_exact( - self, - "mix_ensemble", - (bs, 16), - bs, - np.int32, - np.float32, - np.float32, - output0_raw=False, - output1_raw=False, - use_http=USE_HTTP, - use_grpc=USE_GRPC, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) - - if "onnx" in BACKENDS: - # If label file is provided, it will use the provided label file directly - try: - iu.infer_exact( - self, - "wrong_label", - (1, 16), - 1, - np.int32, - np.float32, - np.float32, - output0_raw=False, - output1_raw=False, - use_http=USE_HTTP, - use_grpc=USE_GRPC, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) - except AssertionError: - # Sanity check that infer_exact failed since this ensemble is provided - # with unexpected labels - pass - - if "onnx" in BACKENDS: - for bs in (1, 8): - iu.infer_exact( - self, - "label_override", - (bs, 16), - bs, - np.int32, - np.float32, - np.float32, - output0_raw=False, - output1_raw=False, - use_http=USE_HTTP, - use_grpc=USE_GRPC, - use_system_shared_memory=TEST_SYSTEM_SHARED_MEMORY, - use_cuda_shared_memory=TEST_CUDA_SHARED_MEMORY, - ) - if __name__ == "__main__": unittest.main() diff --git a/qa/L0_infer/test.sh b/qa/L0_infer/test.sh index 691eca4b72..79ba093ffd 100755 --- a/qa/L0_infer/test.sh +++ b/qa/L0_infer/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -73,7 +73,7 @@ fi if [ "$TEST_SYSTEM_SHARED_MEMORY" -eq 1 ] || [ "$TEST_CUDA_SHARED_MEMORY" -eq 1 ]; then EXPECTED_NUM_TESTS=${EXPECTED_NUM_TESTS:="33"} else - EXPECTED_NUM_TESTS=${EXPECTED_NUM_TESTS:="46"} + EXPECTED_NUM_TESTS=${EXPECTED_NUM_TESTS:="44"} fi TEST_JETSON=${TEST_JETSON:=0} @@ -109,9 +109,6 @@ fi # Allow more time to exit. Ensemble brings in too many models SERVER_ARGS_EXTRA="--exit-timeout-secs=${SERVER_TIMEOUT} --backend-directory=${BACKEND_DIR} --backend-config=python,stub-timeout-seconds=120 --backend-config=python,shm-default-byte-size=${DEFAULT_SHM_SIZE_BYTES}" -if [ "$TEST_SYSTEM_SHARED_MEMORY" -eq 1 ] || [ "$TEST_CUDA_SHARED_MEMORY" -eq 1 ]; then - SERVER_ARGS_EXTRA="${SERVER_ARGS_EXTRA} --allow-client-shm=true" -fi SERVER_ARGS="--model-repository=${MODELDIR} ${SERVER_ARGS_EXTRA}" SERVER_LOG_BASE="./inference_server" source ../common/util.sh @@ -163,9 +160,6 @@ function generate_model_repository() { # Types that need to use SubAdd instead of AddSub swap_types="float32 int32 int16 int8" for onnx_model in $onnx_models; do - # Skip BF16 models: Not yet supported by Python backend - [[ "$onnx_model" == *bf16* ]] && continue - if [ "$BACKEND" == "python_dlpack" ]; then python_model=`echo $onnx_model | sed 's/onnx/python_dlpack/g' | sed 's,'"$DATADIR/qa_model_repository/"',,g'` else @@ -218,10 +212,6 @@ function generate_model_repository() { else cp -r ${DATADIR}/qa_model_repository/${BACKEND}* \ models/. - # Remove ONNX BF16 models from CPU models - if [ "$BACKEND" == "onnx" ] && [ "$TARGET" == "cpu" ]; then - rm -rf models/onnx_*bf16* - fi fi done @@ -260,24 +250,20 @@ function generate_model_repository() { KIND="KIND_GPU" && [[ "$TARGET" == "cpu" ]] && KIND="KIND_CPU" for FW in $BACKENDS; do - [ "$FW" == "plan" ] && continue - for MC in `ls models/${FW}*/config.pbtxt`; do - # BF16 models: ORT CPU has no BF16 kernels, force GPU - if [ "$FW" == "onnx" ] && [ "$MC" == *bf16* ]; then - MC_KIND="KIND_GPU" - else - MC_KIND=${KIND} - fi - - if [ "$FW" == "onnx" ] && [ "$TEST_VALGRIND" -eq 1 ]; then - # Reduce the instance count to make loading onnx models faster - echo "instance_group [ { kind: ${MC_KIND} count: 1 }]" >> $MC - elif [ "$FW" == "python" ] || [ "$FW" == "python_dlpack" ] || [ "$FW" == "openvino" ]; then - echo "instance_group [ { kind: KIND_CPU }]" >> $MC - else - echo "instance_group [ { kind: ${KIND} }]" >> $MC - fi - done + if [ "$FW" == "onnx" ] && [ "$TEST_VALGRIND" -eq 1 ]; then + # Reduce the instance count to make loading onnx models faster + for MC in `ls models/${FW}*/config.pbtxt`; do + echo "instance_group [ { kind: ${KIND} count: 1 }]" >> $MC + done + elif [ "$FW" != "plan" ] && [ "$FW" != "python" ] && [ "$FW" != "python_dlpack" ] && [ "$FW" != "openvino" ];then + for MC in `ls models/${FW}*/config.pbtxt`; do + echo "instance_group [ { kind: ${KIND} }]" >> $MC + done + elif [ "$FW" == "python" ] || [ "$FW" == "python_dlpack" ] || [ "$FW" == "openvino" ]; then + for MC in `ls models/${FW}*/config.pbtxt`; do + echo "instance_group [ { kind: KIND_CPU }]" >> $MC + done + fi done # Modify custom_zero_1_float32 and custom_nobatch_zero_1_float32 for relevant ensembles @@ -363,7 +349,7 @@ done # separately to reduce the loading time. if [ "$TEST_VALGRIND" -eq 1 ]; then TESTING_BACKENDS="python python_dlpack onnx" - EXPECTED_NUM_TESTS=36 + EXPECTED_NUM_TESTS=42 if [[ "aarch64" != $(uname -m) ]] ; then pip3 install torch==2.3.1+cpu -f https://download.pytorch.org/whl/torch_stable.html else @@ -378,11 +364,6 @@ if [ "$TEST_VALGRIND" -eq 1 ]; then mkdir nobatch_models mv ./models/*nobatch_* ./nobatch_models/. cp -fr ./models/nop_* ./nobatch_models/. - if [[ $BACKENDS == *"onnx"* ]]; then - # These two models are required by test_ensemble_mix_batch_nobatch test case. - cp -fr ./models/onnx_float32_float32_float32 ./nobatch_models/. - cp -fr ./models/custom_zero_1_float32 ./nobatch_models/. - fi for BATCHING_MODE in batch nobatch; do if [ "$TRITON_SERVER_CPU_ONLY" == "1" ]; then @@ -422,7 +403,7 @@ if [ "$TEST_VALGRIND" -eq 1 ]; then set +e - VALGRIND_TESTS="1" python3 $INFER_TEST >$CLIENT_LOG 2>&1 + python3 $INFER_TEST >$CLIENT_LOG 2>&1 if [ $? -ne 0 ]; then cat $CLIENT_LOG RET=1 diff --git a/qa/L0_infer_reshape/infer_reshape_test.py b/qa/L0_infer_reshape/infer_reshape_test.py index d3adcfbbeb..5445f5d6c9 100755 --- a/qa/L0_infer_reshape/infer_reshape_test.py +++ b/qa/L0_infer_reshape/infer_reshape_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -50,7 +50,9 @@ def _full_reshape(self, dtype, input_shapes, output_shapes=None, no_batch=True): output_shapes = input_shapes # For validation assume any shape can be used... - if tu.validate_for_onnx_model(dtype, dtype, dtype): + if tu.validate_for_onnx_model( + dtype, dtype, dtype, input_shapes[0], input_shapes[0], input_shapes[0] + ): # model that supports batching for bs in (1, 8): full_shapes = [ @@ -153,6 +155,9 @@ def _full_reshape(self, dtype, input_shapes, output_shapes=None, no_batch=True): dtype, dtype, dtype, + input_shapes[0], + input_shapes[0], + input_shapes[0], ): # model that supports batching for bs in (1, 8): @@ -198,7 +203,9 @@ def _trt_reshape(self, dtype, input_shapes, output_shapes=None, no_batch=True): if output_shapes is None: output_shapes = input_shapes - if tu.validate_for_trt_model(dtype, dtype, dtype): + if tu.validate_for_trt_model( + dtype, dtype, dtype, input_shapes[0], input_shapes[0], input_shapes[0] + ): # model that supports batching for bs in (1, 8): full_shapes = [ diff --git a/qa/L0_infer_variable/infer_variable_test.py b/qa/L0_infer_variable/infer_variable_test.py index a3dce782df..55a2c7a084 100755 --- a/qa/L0_infer_variable/infer_variable_test.py +++ b/qa/L0_infer_variable/infer_variable_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -140,6 +140,9 @@ def _infer_exact_helper( input_dtype, output0_dtype, output1_dtype, + input_shape, + input_shape, + input_shape, ): ensemble_prefix.append(prefix) @@ -147,6 +150,9 @@ def _infer_exact_helper( input_dtype, output0_dtype, output1_dtype, + input_shape, + output0_shape, + output1_shape, ): for prefix in ensemble_prefix: if input_dtype == np.int8: @@ -180,6 +186,9 @@ def _infer_exact_helper( input_dtype, output0_dtype, output1_dtype, + input_shape, + output0_shape, + output1_shape, ): # No basic ensemble models are created against custom models [TODO] _infer_exact_helper( diff --git a/qa/L0_infer_variable/test.sh b/qa/L0_infer_variable/test.sh index 4498f1a57f..36d86716ad 100755 --- a/qa/L0_infer_variable/test.sh +++ b/qa/L0_infer_variable/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2019-2026, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -64,11 +64,6 @@ for TARGET in cpu gpu; do cp -r /data/inferenceserver/${REPO_VERSION}/qa_variable_model_repository models && \ cp -r /data/inferenceserver/${REPO_VERSION}/qa_ensemble_model_repository/qa_variable_model_repository/* models/. - # Remove ONNX BF16 models from CPU models - if [ "$TARGET" == "cpu" ]; then - rm -rf models/onnx_*bf16* - fi - create_nop_version_dir `pwd`/models KIND="KIND_GPU" && [[ "$TARGET" == "cpu" ]] && KIND="KIND_CPU" diff --git a/qa/L0_infer_zero/infer_zero_test.py b/qa/L0_infer_zero/infer_zero_test.py index 89c77f2554..d05b383cff 100755 --- a/qa/L0_infer_zero/infer_zero_test.py +++ b/qa/L0_infer_zero/infer_zero_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -93,7 +93,9 @@ def _full_zero(self, dtype, shapes): ) for name in ["simple_zero", "sequence_zero", "fan_zero"]: - if tu.validate_for_ensemble_model(name, dtype, dtype, dtype): + if tu.validate_for_ensemble_model( + name, dtype, dtype, dtype, shapes[0], shapes[0], shapes[0] + ): # model that supports batching for bs in (1, 8): batch_shapes = [ diff --git a/qa/L0_input_validation/input_validation_test.py b/qa/L0_input_validation/input_validation_test.py index 0ac4dbe1b8..e3524247aa 100755 --- a/qa/L0_input_validation/input_validation_test.py +++ b/qa/L0_input_validation/input_validation_test.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -288,90 +288,5 @@ def inference_helper(model_name, batch_size=1): inference_helper(model_name="plan_zero_1_float32_int32", batch_size=8) -class ModelNameValidationTest(unittest.TestCase): - INVALID_TRAVERSAL_NAMES = [ - "../etc", - "a/../b", - "../../etc/passwd", - "../../../../etc", - "model/..", - "..", - "/etc/passwd", - "model/subdir", - "model/", - " ..", - ".. ", - ] - - def test_model_name_invalid_load(self): - client = tritongrpcclient.InferenceServerClient("localhost:8001") - for model_name in self.INVALID_TRAVERSAL_NAMES: - print(f"Testing model name: {model_name!r}") - with self.assertRaises(InferenceServerException) as cm: - client.load_model(model_name) - self.assertIn( - "model name must not contain path traversal characters", - str(cm.exception), - f"Expected traversal rejection for model name: {model_name!r}", - ) - - def test_model_name_empty_load(self): - client = tritongrpcclient.InferenceServerClient("localhost:8001") - with self.assertRaises(InferenceServerException) as cm: - client.load_model("") - self.assertIn( - "Model name cannot be empty. Please enter a valid name to deploy.", - str(cm.exception), - ) - - def test_model_name_whitespace_only_load(self): - client = tritongrpcclient.InferenceServerClient("localhost:8001") - whitespace_names = [" ", " ", "\t", "\n", "\r", "\f", "\v", " \t \n "] - for model_name in whitespace_names: - with self.assertRaises(InferenceServerException) as cm: - client.load_model(model_name) - self.assertIn( - "Model name cannot be empty. Please enter a valid name to deploy.", - str(cm.exception), - f"Expected whitespace-only rejection for model name: {model_name!r}", - ) - - def test_model_name_invalid_unload(self): - # Unload should not trigger traversal check - client = tritongrpcclient.InferenceServerClient("localhost:8001") - for model_name in self.INVALID_TRAVERSAL_NAMES: - try: - client.unload_model(model_name) - except InferenceServerException as e: - self.assertNotIn( - "model name must not contain path traversal characters", - str(e), - f"Unload should not trigger traversal rejection for model name: {model_name!r}", - ) - - def test_model_name_valid(self): - """Verify that a syntactically valid model name is not rejected by - the traversal check -- it should fail with a model not found error instead.""" - VALID_MODEL_NAMES = [ - "model123", - # "model OAI", TRI-769: Fix this test case - "model.version1", - "...", - "..my_model", - "model..1", - "model....1", - ] - client = tritongrpcclient.InferenceServerClient("localhost:8001") - for model_name in VALID_MODEL_NAMES: - with self.assertRaises(InferenceServerException) as cm: - client.load_model(model_name) - self.assertNotIn( - "path traversal characters", - str(cm.exception), - "Valid model name should not trigger path traversal rejection", - ) - self.assertIn("failed to poll from model repository", str(cm.exception)) - - if __name__ == "__main__": unittest.main() diff --git a/qa/L0_input_validation/test.sh b/qa/L0_input_validation/test.sh index 767d49988d..2d2cf515ce 100755 --- a/qa/L0_input_validation/test.sh +++ b/qa/L0_input_validation/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -48,6 +48,8 @@ CLIENT_LOG="./input_validation_client.log" TEST_PY=./input_validation_test.py TEST_RESULT_FILE='./test_results.txt' SERVER_LOG="./inference_server.log" +TEST_LOG="./input_byte_size_test.log" +TEST_EXEC=./input_byte_size_test export CUDA_VISIBLE_DEVICES=0 @@ -126,7 +128,7 @@ cp -r $DATADIR/qa_model_repository/onnx_object_int32_int32 models/. cp -r $DATADIR/qa_shapetensor_model_repository/plan_nobatch_zero_1_float32_int32 models/. cp -r $DATADIR/qa_shapetensor_model_repository/plan_zero_1_float32_int32 models/. -SERVER_ARGS="--model-repository=`pwd`/models --allow-client-shm=true" +SERVER_ARGS="--model-repository=`pwd`/models" run_server if [ "$SERVER_PID" == "0" ]; then echo -e "\n***\n*** Failed to start $SERVER\n***" @@ -149,9 +151,7 @@ kill $SERVER_PID wait $SERVER_PID # input_byte_size_test -TEST_LOG="./input_byte_size_test.log" -TEST_EXEC=./input_byte_size_test -cp -r /data/inferenceserver/${REPO_VERSION}/qa_identity_model_repository/{onnx_zero_1_float32,onnx_zero_1_object,onnx_zero_1_bool} ./models +cp -r /data/inferenceserver/${REPO_VERSION}/qa_identity_model_repository/{onnx_zero_1_float32,onnx_zero_1_object} ./models set +e LD_LIBRARY_PATH=/opt/tritonserver/lib:$LD_LIBRARY_PATH $TEST_EXEC >> $TEST_LOG 2>&1 @@ -162,45 +162,6 @@ if [ $? -ne 0 ]; then fi set -e -# tensor_size_test -TEST_LOG="./tensor_size_test.log" -TEST_EXEC=./tensor_size_test - -set +e -LD_LIBRARY_PATH=/opt/tritonserver/lib:$LD_LIBRARY_PATH $TEST_EXEC >> $TEST_LOG 2>&1 -if [ $? -ne 0 ]; then - cat $TEST_LOG - echo -e "\n***\n*** tensor_size_test FAILED\n***" - RET=1 -fi -set -e - -# Model name validation test -rm -rf test_models ; mkdir -p test_models -SERVER_LOG="./model_name_validation_server.log" -CLIENT_LOG="./model_name_validation_client.log" -SERVER_ARGS="--model-repository=`pwd`/test_models --model-control-mode=explicit --log-verbose=1" -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 -fi - -set +e -python3 -m pytest -s --junitxml="model_name_validation.report.xml" $TEST_PY::ModelNameValidationTest >> $CLIENT_LOG 2>&1 - -if [ $? -ne 0 ]; then - cat $CLIENT_LOG - cat $SERVER_LOG - echo -e "\n***\n*** input_validation_test.py::ModelNameValidationTest FAILED. \n***" - RET=1 -fi -set -e - -kill $SERVER_PID -wait $SERVER_PID - if [ $RET -eq 0 ]; then echo -e "\n***\n*** Input Validation Test Passed\n***" else diff --git a/qa/L0_io/test.sh b/qa/L0_io/test.sh index 43c7e7dd41..10318da4db 100755 --- a/qa/L0_io/test.sh +++ b/qa/L0_io/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -55,7 +55,7 @@ LD_LIBRARY_PATH=/opt/tritonserver/lib:$LD_LIBRARY_PATH rm -f $CLIENT_LOG* # PyTorch is required for the Python backend dlpack add sub models -pip3 install torch -f https://download.pytorch.org/whl/cu130 +pip3 install torch==2.3.1+cu118 -f https://download.pytorch.org/whl/torch_stable.html RET=0 # Prepare float32 models with basic config diff --git a/qa/L0_libtorch_instance_group_kind_model/test.sh b/qa/L0_libtorch_instance_group_kind_model/test.sh index a68a8b3257..9cbb6bf550 100755 --- a/qa/L0_libtorch_instance_group_kind_model/test.sh +++ b/qa/L0_libtorch_instance_group_kind_model/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -39,7 +39,7 @@ if [ ! -z "$TEST_REPO_ARCH" ]; then fi pip3 uninstall -y torch -pip3 install torch -f https://download.pytorch.org/whl/cu130 +pip3 install torch==2.3.1+cu118 -f https://download.pytorch.org/whl/torch_stable.html DATADIR=/data/inferenceserver/${REPO_VERSION}/qa_model_repository SERVER=/opt/tritonserver/bin/tritonserver diff --git a/qa/L0_lifecycle/lifecycle_test.py b/qa/L0_lifecycle/lifecycle_test.py index 2dba1c25d5..8ad95e3d90 100755 --- a/qa/L0_lifecycle/lifecycle_test.py +++ b/qa/L0_lifecycle/lifecycle_test.py @@ -2669,8 +2669,7 @@ def callback(user_data, result, error): self.assertTrue(False, "expected error for new inference during shutdown") except InferenceServerException as ex: self.assertIn( - "failed to connect to all addresses; last error: UNKNOWN: ipv4:127.0.0.1:8001: " - + "Failed to connect to remote host: connect: Connection refused (111)", + "Server is stopping, scheduler for model has stopped accepting new inference requests", ex.message(), ) @@ -2732,29 +2731,33 @@ def callback(user_data, result, error): ) self.assertTrue(False, "expected error for new inference during shutdown") except InferenceServerException as ex: - # The first request received by the gRPC endpoint while shutting down returns CANCELLED - # each subsequent request returns Connection refused - self.assertIn("CANCELLED", ex.message()) + self.assertIn( + "Server is stopping, scheduler for model has stopped accepting new inference requests", + ex.message(), + ) # 2: New sequence with existing sequence ID try: triton_client.infer(model_name, inputs, sequence_id=1, sequence_start=True) self.assertTrue(False, "expected error for new inference during shutdown") except InferenceServerException as ex: self.assertIn( - "failed to connect to all addresses; last error: UNKNOWN: ipv4:127.0.0.1:8001: " - + "Failed to connect to remote host: connect: Connection refused (111)", + "Server is stopping, scheduler for model has stopped accepting new inference requests", ex.message(), ) - # 3: Continuing sequence after shutdown + # 3: Continuing sequence try: - triton_client.infer(model_name, inputs, sequence_id=2, sequence_end=True) - self.assertTrue(False, "expected error for new inference during shutdown") - except InferenceServerException as ex: - self.assertIn( - "failed to connect to all addresses; last error: UNKNOWN: ipv4:127.0.0.1:8001: " - + "Failed to connect to remote host: connect: Connection refused (111)", - ex.message(), + res = triton_client.infer( + model_name, inputs, sequence_id=2, sequence_end=True ) + output_data = res.as_numpy("OUTPUT") + # Result are accumulated + np.testing.assert_allclose( + output_data, + input_data + input_data, + err_msg="Inference result is not correct", + ) + except Exception as ex: + self.assertTrue(False, "unexpected error {}".format(ex)) # Wait until the results are available in user_data time_out = 30 @@ -2813,9 +2816,9 @@ def callback(user_data, result, error): triton_client.infer(model_name, inputs) self.assertTrue(False, "expected error for new inference during shutdown") except InferenceServerException as ex: + self.assertIn("in ensemble 'ensemble_zero_1_float32'", ex.message()) self.assertIn( - "failed to connect to all addresses; last error: UNKNOWN: ipv4:127.0.0.1:8001: " - + "Failed to connect to remote host: connect: Connection refused (111)", + "Server is stopping, scheduler for model has stopped accepting new inference requests", ex.message(), ) @@ -3385,7 +3388,7 @@ def test_shutdown_with_live_connection(self): # close connection conn.close() - time.sleep(3) + time.sleep(2) # check exit timeout countdown did not restart with open(os.environ["SERVER_LOG"]) as f: diff --git a/qa/L0_lifecycle/test.sh b/qa/L0_lifecycle/test.sh index 964986a519..bcd1713bb8 100755 --- a/qa/L0_lifecycle/test.sh +++ b/qa/L0_lifecycle/test.sh @@ -1579,8 +1579,8 @@ check_unit_test set -e # check server log -if [ `grep -c "Found 1 gRPC service connections and inference handlers" $SERVER_LOG` == "0" ]; then - echo -e "\n***\n*** Expect logging for in-flight gRPC connection count\n***" +if [ `grep -c "Model 'custom_zero_1_float32' (version 1) has 1 in-flight inferences" $SERVER_LOG` == "0" ]; then + echo -e "\n***\n*** Expect logging for model and in-flight inference count\n***" RET=1 fi @@ -1617,6 +1617,10 @@ if [ `grep -c "Model 'custom_sequence_int32' (version 1) has 2 in-flight inferen echo -e "\n***\n*** Expect logging for model having 2 in-flight inferences\n***" RET=1 fi +if [ `grep -c "Model 'custom_sequence_int32' (version 1) has 1 in-flight inferences" $SERVER_LOG` == "0" ]; then + echo -e "\n***\n*** Expect logging for model having 1 in-flight inference\n***" + RET=1 +fi kill $SERVER_PID || true wait $SERVER_PID @@ -1654,8 +1658,8 @@ check_unit_test set -e # check server log -if [ `grep -c "Found 1 gRPC service connections and inference handlers" $SERVER_LOG` == "0" ]; then - echo -e "\n***\n*** Expect logging for in-flight gRPC connection count\n***" +if [ `grep -c "Model 'ensemble_zero_1_float32' (version 1) has 1 in-flight inferences" $SERVER_LOG` == "0" ]; then + echo -e "\n***\n*** Expect logging for model and in-flight inference count\n***" RET=1 fi @@ -1666,7 +1670,7 @@ LOG_IDX=$((LOG_IDX+1)) # LifeCycleTest.test_load_gpu_limit # dependency of the Python model to be used -pip install "cuda-python>=12,<13" +pip install cuda-python rm -fr models config.pbtxt.* mkdir models cp -r ../python_models/cuda_memory_consumer models/cuda_memory_consumer_1 && \ diff --git a/qa/L0_logging/test.sh b/qa/L0_logging/test.sh index 4dabe976e6..a25693cf0e 100755 --- a/qa/L0_logging/test.sh +++ b/qa/L0_logging/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -490,7 +490,7 @@ set -e kill $SERVER_PID wait $SERVER_PID -# Test Negative Test Cases +#Test Negative Test Cases SERVER_ARGS="--log-warn="false" --model-repository=$MODELSDIR" SERVER_LOG="./server.log" run_server @@ -607,53 +607,6 @@ fi set -e -# Test Log Output Stream -# Set up an invalid model with a leading zero in the version number. This will print warning and error logs. -MODELSDIR_INVALID=`pwd`/log_models_invalid -rm -rf $MODELSDIR_INVALID && \ - cp -r $MODELSDIR $MODELSDIR_INVALID && \ - mv $MODELSDIR_INVALID/simple/1 $MODELSDIR_INVALID/simple/01 - -rm -f log_file.log -LOG_REGEX="(?P\d{2})(?P\d{2}) (?P\d{2}:\d{2}:\d{2}\.\d{6}) (?P\d+) (?P[\w\.]+):(?P\d+)] (?P.*)" -SERVER_ARGS="--log-verbose=1 --model-repository=$MODELSDIR_INVALID" -SERVER_LOG="./inference_server_log_file.log" -SERVER_ERROR_LOG="./inference_server_error_log_file.log" -run_server -if [ "$SERVER_PID" != "0" ]; then - echo -e "*** FAILED: unexpected success starting $SERVER" >> $CLIENT_LOG - cat $SERVER_LOG - kill_server - exit 1 -fi - -set +e -# Only INFO logs in SERVER_LOG -if [ `grep -c -P "I$LOG_REGEX" $SERVER_LOG` == "0" ]; then - echo -e "\n***\n*** Test Failed: INFO logs are not written to $SERVER_LOG\n***" - RET=1 -fi -if [ `grep -c -P "(W|E)$LOG_REGEX" $SERVER_LOG` != "0" ]; then - echo -e "\n***\n*** Test Failed: WARNING/ERROR logs are written to $SERVER_LOG\n***" - RET=1 -fi -# Only WARNING and ERROR logs in SERVER_ERROR_LOG -if [ `grep -c -P "I$LOG_REGEX" $SERVER_ERROR_LOG` != "0" ]; then - echo -e "\n***\n*** Test Failed: INFO logs are written to $SERVER_ERROR_LOG\n***" - RET=1 -fi -if [ `grep -c -P "W$LOG_REGEX" $SERVER_ERROR_LOG` == "0" ]; then - echo -e "\n***\n*** Test Failed: ERROR logs are not written to $SERVER_ERROR_LOG\n***" - RET=1 -fi -if [ `grep -c -P "E$LOG_REGEX" $SERVER_ERROR_LOG` == "0" ]; then - echo -e "\n***\n*** Test Failed: ERROR logs are not written to $SERVER_ERROR_LOG\n***" - RET=1 -fi - -unset $SERVER_ERROR_LOG -set -e - if [ $RET -eq 0 ]; then echo -e "\n***\n*** Test Passed\n***" else diff --git a/qa/L0_long_running_stress/scenarios.py b/qa/L0_long_running_stress/scenarios.py index 342b8a6937..650f228ab5 100755 --- a/qa/L0_long_running_stress/scenarios.py +++ b/qa/L0_long_running_stress/scenarios.py @@ -99,7 +99,7 @@ def run(self, client_metadata): class PerfAnalyzerScenario(Scenario): # Some class static variables - command_ = "perf_analyzer" + command_ = "../clients/perf_analyzer" generation_mutex_ = threading.Lock() class ModelOption: @@ -203,7 +203,7 @@ def __init__( # Add no validation models self.options_.append( PerfAnalyzerScenario.ModelOption( - "resnet_v1_50", 32, (1, 4, 1), queue_latency_range_us + "resnet_v1_50_def", 32, (1, 4, 1), queue_latency_range_us ) ) for trial in sequence_trials: @@ -334,7 +334,7 @@ def run(self, client_metadata): class ResNetScenario(Scenario): def __init__(self, name, batch_size=32, verbose=False, out_stream=sys.stdout): super().__init__(name, [], verbose, out_stream) - self.model_name_ = "resnet_v1_50" + self.model_name_ = "resnet_v1_50_def" self.batch_size_ = batch_size img = self.preprocess("../images/vulture.jpeg") @@ -353,7 +353,7 @@ def preprocess(self, filename): return scaled def postprocess(self, results): - output_array = results.as_numpy("resnet_v1_50/predictions/Softmax:0") + output_array = results.as_numpy("resnet_v1_50/predictions/Softmax") if len(output_array) != self.batch_size_: raise Exception( "expected {} results, got {}".format( @@ -377,12 +377,12 @@ def postprocess(self, results): def run(self, client_metadata): triton_client = client_metadata[0] - inputs = [grpcclient.InferInput("input:0", self.image_data_.shape, "FP32")] + inputs = [grpcclient.InferInput("input", self.image_data_.shape, "FP32")] inputs[0].set_data_from_numpy(self.image_data_) outputs = [ grpcclient.InferRequestedOutput( - "resnet_v1_50/predictions/Softmax:0", class_count=1 + "resnet_v1_50/predictions/Softmax", class_count=1 ) ] res = triton_client.infer(self.model_name_, inputs, outputs=outputs) diff --git a/qa/L0_long_running_stress/test.sh b/qa/L0_long_running_stress/test.sh index 92d8c09b0d..1973028bcf 100755 --- a/qa/L0_long_running_stress/test.sh +++ b/qa/L0_long_running_stress/test.sh @@ -130,8 +130,9 @@ cp -r ../custom_models/custom_zero_1_float32 $MODEL_DIR/custom_zero_1_float32 && echo "{ key: \"execute_delay_ms\"; value: { string_value: \"10000\" }}" >> config.pbtxt && \ echo "]" >> config.pbtxt) -cp -r $DATADIR/onnx_model_store/resnet_v1_50 $MODEL_DIR/. && \ - (cd $MODEL_DIR/resnet_v1_50 && \ +cp -r $DATADIR/onnx_model_store/resnet_v1_50 $MODEL_DIR/resnet_v1_50_def && \ + (cd $MODEL_DIR/resnet_v1_50_def && \ + sed -i 's/^name: "resnet_v1_50"/name: "resnet_v1_50_def"/' config.pbtxt && \ echo "optimization { }" >> config.pbtxt) SERVER_ARGS="--model-repository=`pwd`/$MODEL_DIR" diff --git a/qa/L0_memory_growth/test.sh b/qa/L0_memory_growth/test.sh index f23813937d..fe4911c9bc 100755 --- a/qa/L0_memory_growth/test.sh +++ b/qa/L0_memory_growth/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2020-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -42,8 +42,7 @@ fi export CUDA_VISIBLE_DEVICES=0 # Clients -pip3 install perf_analyzer -PERF_ANALYZER=perf_analyzer +PERF_ANALYZER=../clients/perf_analyzer IMAGE=../images/vulture.jpeg # Models @@ -102,7 +101,7 @@ export MAX_ALLOWED_ALLOC="100" # Create local model repository mkdir -p models/ -cp -r $DATADIR/perf_model_store/resnet50_* models/ +cp -r $DATADIR/perf_model_store/resnet50* models/ # Create the TensorRT plan from ONNX model rm -fr models/resnet50_fp32_plan && mkdir -p models/resnet50_fp32_plan/1 && \ @@ -132,9 +131,7 @@ RET=0 for MODEL in $(ls models); do # Skip the resnet50_fp32_libtorch model as it is running into `misaligned address' # Tracked here: https://nvbugs/3954104 - # Skip the resnet50_fp32_onnx model as the inference hangs on A100 with batch size > 1. - # Tracked here: https://linear.app/nvidia/issue/TRI-304 - if [[ "$MODEL" == "resnet50_fp32_libtorch" || "$MODEL" == "resnet50_fp32_onnx" ]]; then + if [ "$MODEL" == "resnet50_fp32_libtorch" ]; then continue fi diff --git a/qa/L0_metrics/ensemble_decoupled/async_execute_decouple/1/model.py b/qa/L0_metrics/ensemble_decoupled/async_execute_decouple/1/model.py index 73454c5a3c..6a73e12da4 100644 --- a/qa/L0_metrics/ensemble_decoupled/async_execute_decouple/1/model.py +++ b/qa/L0_metrics/ensemble_decoupled/async_execute_decouple/1/model.py @@ -1,4 +1,4 @@ -# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -45,9 +45,9 @@ async def execute(self, requests): ] # Wait + time.sleep(wait_secs.item()) response_sender = request.get_response_sender() for i in range(response_num): - time.sleep(wait_secs.item()) response = pb_utils.InferenceResponse(output_tensors) if i != response_num - 1: response_sender.send(response) diff --git a/qa/L0_metrics/histogram_metrics_test.py b/qa/L0_metrics/histogram_metrics_test.py index 725f86fcbe..a59aac0478 100755 --- a/qa/L0_metrics/histogram_metrics_test.py +++ b/qa/L0_metrics/histogram_metrics_test.py @@ -1,5 +1,5 @@ #!/usr/bin/python -# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -114,7 +114,7 @@ def callback(user_data, result, error): def test_ensemble_decoupled(self): wait_secs = 1 responses_per_req = 3 - total_iters = 3 + total_reqs = 3 delta = 0.2 # Infer @@ -133,7 +133,7 @@ def test_ensemble_decoupled(self): inputs[1].set_data_from_numpy(input_data_1) # Send requests to ensemble decoupled model - for iter_cnt in range(1, total_iters + 1): + for request_num in range(1, total_reqs + 1): ensemble_model_name = "ensemble" decoupled_model_name = "async_execute_decouple" non_decoupled_model_name = "async_execute" @@ -144,40 +144,40 @@ def test_ensemble_decoupled(self): # Checks metrics output histogram_dict = self.get_histogram_metrics(FIRST_RESPONSE_HISTOGRAM) - def check_histogram(model_name, request_cnt, wait_secs_per_req, delta): - histogram_count_key = get_histogram_metric_key( + def check_existing_metrics(model_name, wait_secs_per_req, delta): + metric_count = get_histogram_metric_key( FIRST_RESPONSE_HISTOGRAM, model_name, "1", "count" ) - histogram_sum_key = get_histogram_metric_key( + metric_sum = get_histogram_metric_key( FIRST_RESPONSE_HISTOGRAM, model_name, "1", "sum" ) # Test histogram count - self.assertIn(histogram_count_key, histogram_dict) - self.assertEqual( - histogram_dict[histogram_count_key], request_cnt * iter_cnt - ) + self.assertIn(metric_count, histogram_dict) + self.assertEqual(histogram_dict[metric_count], request_num) # Test histogram sum - self.assertIn(histogram_sum_key, histogram_dict) + self.assertIn(metric_sum, histogram_dict) self.assertTrue( - wait_secs_per_req * MILLIS_PER_SEC * request_cnt * iter_cnt - <= histogram_dict[histogram_sum_key] - < (wait_secs_per_req + delta) - * MILLIS_PER_SEC - * request_cnt - * iter_cnt + wait_secs_per_req * MILLIS_PER_SEC * request_num + <= histogram_dict[metric_sum] + < (wait_secs_per_req + delta) * MILLIS_PER_SEC * request_num ) # Prometheus histogram buckets are tested in metrics_api_test.cc::HistogramAPIHelper # Test ensemble model metrics - check_histogram(ensemble_model_name, 1, wait_secs * 2, 2 * delta) + check_existing_metrics(ensemble_model_name, 2 * wait_secs, 2 * delta) # Test decoupled model metrics - check_histogram(decoupled_model_name, 1, wait_secs, delta) + check_existing_metrics(decoupled_model_name, wait_secs, delta) # Test non-decoupled model metrics - check_histogram( - non_decoupled_model_name, responses_per_req, wait_secs, delta + non_decoupled_model_count = get_histogram_metric_key( + FIRST_RESPONSE_HISTOGRAM, non_decoupled_model_name, "1", "count" + ) + non_decoupled_model_sum = get_histogram_metric_key( + FIRST_RESPONSE_HISTOGRAM, non_decoupled_model_name, "1", "sum" ) + self.assertNotIn(non_decoupled_model_count, histogram_dict) + self.assertNotIn(non_decoupled_model_sum, histogram_dict) def test_buckets_override(self): model_name = "async_execute_decouple" diff --git a/qa/L0_metrics/metrics_config_test.py b/qa/L0_metrics/metrics_config_test.py index 0c448dbaa7..975d219ef8 100755 --- a/qa/L0_metrics/metrics_config_test.py +++ b/qa/L0_metrics/metrics_config_test.py @@ -1,5 +1,5 @@ #!/usr/bin/python -# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -44,7 +44,7 @@ "nv_inference_compute_infer_duration", "nv_inference_compute_output_duration", ] -INF_HISTOGRAM_PATTERNS = ["nv_inference_first_response_histogram_ms"] +INF_HISTOGRAM_DECOUPLED_PATTERNS = ["nv_inference_first_response_histogram_ms"] INF_SUMMARY_PATTERNS = [ "nv_inference_request_summary", "nv_inference_queue_summary", @@ -99,15 +99,15 @@ def test_cache_counters_missing(self): self.assertNotIn(metric, metrics) # Histograms - def test_inf_histograms_exist(self): + def test_inf_histograms_decoupled_exist(self): metrics = self._get_metrics() - for metric in INF_HISTOGRAM_PATTERNS: + for metric in INF_HISTOGRAM_DECOUPLED_PATTERNS: for suffix in ["_count", "_sum", "_bucket"]: self.assertIn(metric + suffix, metrics) - def test_inf_histograms_missing(self): + def test_inf_histograms_decoupled_missing(self): metrics = self._get_metrics() - for metric in INF_HISTOGRAM_PATTERNS: + for metric in INF_HISTOGRAM_DECOUPLED_PATTERNS: self.assertNotIn(metric, metrics) # Summaries diff --git a/qa/L0_metrics/test.sh b/qa/L0_metrics/test.sh index b1f77774a2..2d6e85e211 100755 --- a/qa/L0_metrics/test.sh +++ b/qa/L0_metrics/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2020-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -276,7 +276,7 @@ SERVER_ARGS="${BASE_SERVER_ARGS} --load-model=identity_cache_off" run_and_check_server python3 ${PYTHON_TEST} MetricsConfigTest.test_inf_counters_exist 2>&1 | tee ${CLIENT_LOG} check_unit_test -python3 ${PYTHON_TEST} MetricsConfigTest.test_inf_histograms_missing 2>&1 | tee ${CLIENT_LOG} +python3 ${PYTHON_TEST} MetricsConfigTest.test_inf_histograms_decoupled_missing 2>&1 | tee ${CLIENT_LOG} check_unit_test python3 ${PYTHON_TEST} MetricsConfigTest.test_inf_summaries_missing 2>&1 | tee ${CLIENT_LOG} check_unit_test @@ -286,12 +286,47 @@ python3 ${PYTHON_TEST} MetricsConfigTest.test_cache_summaries_missing 2>&1 | tee check_unit_test kill_server -# Enable histograms +# Check default settings: Histograms should be always disabled in non-decoupled model. SERVER_ARGS="${BASE_SERVER_ARGS} --load-model=identity_cache_off --metrics-config histogram_latencies=true" run_and_check_server python3 ${PYTHON_TEST} MetricsConfigTest.test_inf_counters_exist 2>&1 | tee ${CLIENT_LOG} check_unit_test -python3 ${PYTHON_TEST} MetricsConfigTest.test_inf_histograms_exist 2>&1 | tee ${CLIENT_LOG} +python3 ${PYTHON_TEST} MetricsConfigTest.test_inf_histograms_decoupled_missing 2>&1 | tee ${CLIENT_LOG} +check_unit_test +python3 ${PYTHON_TEST} MetricsConfigTest.test_inf_summaries_missing 2>&1 | tee ${CLIENT_LOG} +check_unit_test +python3 ${PYTHON_TEST} MetricsConfigTest.test_cache_counters_missing 2>&1 | tee ${CLIENT_LOG} +check_unit_test +python3 ${PYTHON_TEST} MetricsConfigTest.test_cache_summaries_missing 2>&1 | tee ${CLIENT_LOG} +check_unit_test +kill_server + +# Check default settings: Histograms should be disabled in decoupled model +decoupled_model="async_execute_decouple" +mkdir -p "${MODELDIR}/${decoupled_model}/1/" +cp ../python_models/${decoupled_model}/model.py ${MODELDIR}/${decoupled_model}/1/ +cp ../python_models/${decoupled_model}/config.pbtxt ${MODELDIR}/${decoupled_model}/ + +SERVER_ARGS="${BASE_SERVER_ARGS} --load-model=${decoupled_model}" +run_and_check_server +python3 ${PYTHON_TEST} MetricsConfigTest.test_inf_counters_exist 2>&1 | tee ${CLIENT_LOG} +check_unit_test +python3 ${PYTHON_TEST} MetricsConfigTest.test_inf_histograms_decoupled_missing 2>&1 | tee ${CLIENT_LOG} +check_unit_test +python3 ${PYTHON_TEST} MetricsConfigTest.test_inf_summaries_missing 2>&1 | tee ${CLIENT_LOG} +check_unit_test +python3 ${PYTHON_TEST} MetricsConfigTest.test_cache_counters_missing 2>&1 | tee ${CLIENT_LOG} +check_unit_test +python3 ${PYTHON_TEST} MetricsConfigTest.test_cache_summaries_missing 2>&1 | tee ${CLIENT_LOG} +check_unit_test +kill_server + +# Enable histograms in decoupled model +SERVER_ARGS="${BASE_SERVER_ARGS} --load-model=${decoupled_model} --metrics-config histogram_latencies=true" +run_and_check_server +python3 ${PYTHON_TEST} MetricsConfigTest.test_inf_counters_exist 2>&1 | tee ${CLIENT_LOG} +check_unit_test +python3 ${PYTHON_TEST} MetricsConfigTest.test_inf_histograms_decoupled_exist 2>&1 | tee ${CLIENT_LOG} check_unit_test python3 ${PYTHON_TEST} MetricsConfigTest.test_inf_summaries_missing 2>&1 | tee ${CLIENT_LOG} check_unit_test @@ -430,7 +465,6 @@ SERVER_LOG="./histogram_ensemble_decoupled_server.log" CLIENT_LOG="./histogram_ensemble_decoupled_client.log" SERVER_ARGS="--model-repository=${MODELDIR} --metrics-config histogram_latencies=true --log-verbose=1" mkdir -p "${MODELDIR}"/ensemble/1 -rm -rf "${MODELDIR}"/async_execute cp -r "${MODELDIR}"/async_execute_decouple "${MODELDIR}"/async_execute sed -i "s/model_transaction_policy { decoupled: True }//" "${MODELDIR}"/async_execute/config.pbtxt @@ -476,7 +510,7 @@ kill_server PYTHON_TEST="metrics_config_test.py" SERVER_ARGS="--model-repository=${MODELDIR} --model-control-mode=explicit --load-model=${decoupled_model} --metrics-config histogram_latencies=false --log-verbose=1" run_and_check_server -python3 ${PYTHON_TEST} MetricsConfigTest.test_inf_histograms_missing 2>&1 | tee ${CLIENT_LOG} +python3 ${PYTHON_TEST} MetricsConfigTest.test_inf_histograms_decoupled_missing 2>&1 | tee ${CLIENT_LOG} check_unit_test kill_server diff --git a/qa/L0_mlflow/plugin_test.py b/qa/L0_mlflow/plugin_test.py index e225ed5881..a5d87a3c19 100755 --- a/qa/L0_mlflow/plugin_test.py +++ b/qa/L0_mlflow/plugin_test.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -33,9 +33,7 @@ import json import unittest -import mlflow.onnx import numpy as np -import onnx import test_util as tu from mlflow.deployments import get_deploy_client @@ -47,7 +45,7 @@ def setUp(self): def _validate_deployment(self, model_name): # create self.client_.create_deployment( - model_name, f"models:/{model_name}/1", flavor="onnx" + model_name, "models:/{}/1".format(model_name), flavor="onnx" ) # list @@ -81,6 +79,8 @@ def _validate_deployment(self, model_name): def test_onnx_flavor(self): # Log the ONNX model to MLFlow + import mlflow.onnx + import onnx model = onnx.load( "./mlflow-triton-plugin/examples/onnx_float32_int32_int32/1/model.onnx" @@ -92,6 +92,8 @@ def test_onnx_flavor(self): def test_onnx_flavor_with_files(self): # Log the ONNX model and additional Triton config file to MLFlow + import mlflow.onnx + import onnx model = onnx.load( "./mlflow-triton-plugin/examples/onnx_float32_int32_int32/1/model.onnx" @@ -114,65 +116,6 @@ def test_onnx_flavor_with_files(self): filecmp.cmp(config_path, "./models/onnx_model_with_files/config.pbtxt") ) - def test_model_name(self): - EMPTY_MODEL_NAMES = [ - "", - " ", - " ", - "\n", - "\t", - "\r", - "\v", - "\f", - ] - INVALID_PATH_TRAVERSAL_NAMES = [ - "/opt/sys/", - "../../etc/passwd", - "../outside/repo", - "test_models/../identity_py", - "..", - ] - VALID_MODEL_NAMES = [ - "model123", - # "model OAI", TRI-769: Fix this test case - "model.version1", - "...", - "..my_model", - "model..1", - "model....1", - ] - - for model_name in EMPTY_MODEL_NAMES: - model_uri = f"models:/{model_name}/1" - with self.assertRaises(Exception) as e: - self.client_.create_deployment(model_name, model_uri, flavor="onnx") - self.assertIn( - "Model name cannot be empty. Please enter a valid name to deploy.", - str(e.exception), - ) - - for model_name in INVALID_PATH_TRAVERSAL_NAMES: - model_uri = f"models:/{model_name}/1" - with self.assertRaises(Exception) as e: - self.client_.create_deployment(model_name, model_uri, flavor="onnx") - self.assertIn( - f"Path traversal is not allowed in model's name: {model_name}", - str(e.exception), - ) - - for model_name in VALID_MODEL_NAMES: - model = onnx.load( - "./mlflow-triton-plugin/examples/onnx_float32_int32_int32/1/model.onnx" - ) - - # Use a different name to ensure the plugin operates on correct model - mlflow.onnx.log_model( - model, "triton", registered_model_name=f"{model_name}" - ) - - # Validate deployment functionalities - create, list, get, predict, delete - self._validate_deployment(model_name) - if __name__ == "__main__": unittest.main() diff --git a/qa/L0_mlflow/test.sh b/qa/L0_mlflow/test.sh index 5e863a8988..4b5205ba25 100755 --- a/qa/L0_mlflow/test.sh +++ b/qa/L0_mlflow/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -39,11 +39,7 @@ rm -fr *.log *.json # install a higher version of python which uses blinker 1.6, # but it is unknown whether this test should rely on # the default installation of python. - -apt update -qq && apt install python3-venv -y -python3 -m venv .venv - -source .venv/bin/activate +apt remove -y python3-blinker RET=0 @@ -187,7 +183,7 @@ if [ $? -ne 0 ]; then echo -e "\n***\n*** Python Test Failed\n***" RET=1 else - check_test_results $TEST_RESULT_FILE 3 + check_test_results $TEST_RESULT_FILE 2 if [ $? -ne 0 ]; then cat $PY_LOG echo -e "\n***\n*** Test Result Verification Failed\n***" @@ -256,7 +252,7 @@ if [ $? -ne 0 ]; then echo -e "\n***\n*** Python Test Failed\n***" RET=1 else - check_test_results $TEST_RESULT_FILE 3 + check_test_results $TEST_RESULT_FILE 2 if [ $? -ne 0 ]; then cat $PY_LOG echo -e "\n***\n*** Test Result Verification Failed\n***" diff --git a/qa/L0_model_config/custom_parameters/tensorrt/invalid/allocation_strategy_invalid_value/expected b/qa/L0_model_config/custom_parameters/tensorrt/invalid/allocation_strategy_invalid_value/expected deleted file mode 100644 index 9e66bbafeb..0000000000 --- a/qa/L0_model_config/custom_parameters/tensorrt/invalid/allocation_strategy_invalid_value/expected +++ /dev/null @@ -1 +0,0 @@ -failed to load 'allocation_strategy_invalid_value' version 1: Invalid argument: Invalid value for 'execution_context_allocation_strategy': 'UNKNOWN' for model instance 'allocation_strategy_invalid_value' diff --git a/qa/L0_model_config/custom_parameters/tensorrt/invalid/allocation_strategy_invalid_value/partial.pbtxt b/qa/L0_model_config/custom_parameters/tensorrt/invalid/allocation_strategy_invalid_value/partial.pbtxt deleted file mode 100644 index 2970c9bf23..0000000000 --- a/qa/L0_model_config/custom_parameters/tensorrt/invalid/allocation_strategy_invalid_value/partial.pbtxt +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -parameters: { - key: "execution_context_allocation_strategy" - value: { - string_value: "UNKNOWN" - } -} diff --git a/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_no_key/partial.pbtxt b/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_no_key/partial.pbtxt deleted file mode 100644 index 808432bbc5..0000000000 --- a/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_no_key/partial.pbtxt +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -parameters: {} diff --git a/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_no_parameters/partial.pbtxt b/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_no_parameters/partial.pbtxt deleted file mode 100644 index ce81309581..0000000000 --- a/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_no_parameters/partial.pbtxt +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_1/expected b/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_1/expected deleted file mode 100644 index 584d6becdd..0000000000 --- a/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_1/expected +++ /dev/null @@ -1 +0,0 @@ -'execution_context_allocation_strategy' set to 'STATIC' for model instance 'allocation_strategy_value_1' diff --git a/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_1/partial.pbtxt b/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_1/partial.pbtxt deleted file mode 100644 index ebddccc5b1..0000000000 --- a/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_1/partial.pbtxt +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -parameters: { - key: "execution_context_allocation_strategy" - value: { - string_value: "STATIC" - } -} diff --git a/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_2/expected b/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_2/expected deleted file mode 100644 index 3dcd519ddf..0000000000 --- a/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_2/expected +++ /dev/null @@ -1 +0,0 @@ -'execution_context_allocation_strategy' set to 'ON_PROFILE_CHANGE' for model instance 'allocation_strategy_value_2' diff --git a/qa/L0_model_config/test.sh b/qa/L0_model_config/test.sh index 4ef8f76907..7622cf93b2 100755 --- a/qa/L0_model_config/test.sh +++ b/qa/L0_model_config/test.sh @@ -298,7 +298,7 @@ cp -r /data/inferenceserver/${REPO_VERSION}/qa_model_repository/openvino_int8_in cp /data/inferenceserver/${REPO_VERSION}/qa_model_repository/openvino_int8_int8_int8/output0_labels.txt \ autofill_noplatform_success/openvino/partial_config -# Copy decoupled model and config files into the model_metrics test repository. +# Copy decoupled model into the model_metrics test repository. for modelpath in `ls -d model_metrics/*/*`; do src_dir="/opt/tritonserver/qa/python_models/async_execute_decouple" mkdir -p $modelpath/1 @@ -306,18 +306,6 @@ for modelpath in `ls -d model_metrics/*/*`; do cat $src_dir/config.pbtxt $modelpath/partial.pbtxt > $modelpath/config.pbtxt done -# Copy tensorrt model and config files into the custom_parameters test repository. -for modelpath in `ls -d custom_parameters/tensorrt/*/*`; do - mkdir -p $modelpath/1 - model_name=`basename $modelpath` - src_dir="/data/inferenceserver/${REPO_VERSION}/qa_model_repository/plan_float32_float32_float32" - cp ${src_dir}/1/model.plan $modelpath/1/. - cat ${src_dir}/config.pbtxt $modelpath/partial.pbtxt > $modelpath/config.pbtxt - sed -i "s/^name:.*/name: \"${model_name}\"/" $modelpath/config.pbtxt - sed -i "s/^version_policy:.*//" $modelpath/config.pbtxt - sed -i "s/label_filename:.*//" $modelpath/config.pbtxt -done - rm -f $SERVER_LOG_BASE* $CLIENT_LOG RET=0 @@ -706,82 +694,6 @@ for TARGET_DIR in `ls -d model_metrics/invalid_config/*`; do fi done -# Run all custom_parameters tests that are expected to succeed. -for TARGET_DIR in `ls -d custom_parameters/*/valid/*`; do - TARGET_DIR_DOT=`echo $TARGET_DIR | tr / .` - TARGET=`basename ${TARGET_DIR}` - - SERVER_ARGS="--model-repository=`pwd`/models --log-info=true" - SERVER_LOG=$SERVER_LOG_BASE.${TARGET_DIR_DOT}.log - - rm -fr models && mkdir models - cp -r ${TARGET_DIR} models/. - - EXPECTED=models/$TARGET/expected - echo -e "Test $TARGET_DIR" >> $CLIENT_LOG - - run_server - if [ "$SERVER_PID" == "0" ]; then - echo -e "*** FAILED: unable to start $SERVER" >> $CLIENT_LOG - RET=1 - else - kill $SERVER_PID - wait $SERVER_PID - fi - - if [ -f $EXPECTED ]; then - EX_FOUND=0 - EX=`cat $EXPECTED` - if grep ^I[0-9][0-9][0-9][0-9].*"$EX" $SERVER_LOG; then - echo -e "Found \"$EX\"" >> $CLIENT_LOG - EX_FOUND=1 - else - echo -e "Not found \"$EX\"" >> $CLIENT_LOG - fi - if [ "$EX_FOUND" == "0" ]; then - echo -e "*** FAILED: model_metrics/$TARGET" >> $CLIENT_LOG - RET=1 - fi - fi -done - -# Run all custom_parameters tests that have invalid values. -for TARGET_DIR in `ls -d custom_parameters/*/invalid/*`; do - TARGET_DIR_DOT=`echo $TARGET_DIR | tr / .` - TARGET=`basename ${TARGET_DIR}` - - SERVER_ARGS="--model-repository=`pwd`/models --log-info=true" - SERVER_LOG=$SERVER_LOG_BASE.${TARGET_DIR_DOT}.log - - rm -fr models && mkdir models - cp -r ${TARGET_DIR} models/. - - EXPECTED=models/$TARGET/expected - echo -e "Test $TARGET_DIR" >> $CLIENT_LOG - - # We expect all tests to fail with the expected error message - run_server - if [ "$SERVER_PID" != "0" ]; then - echo -e "*** FAILED: unexpected success starting $SERVER" >> $CLIENT_LOG - RET=1 - kill $SERVER_PID - wait $SERVER_PID - else - EX_FOUND=0 - EX=`cat $EXPECTED` - if grep ^E[0-9][0-9][0-9][0-9].*"$EX" $SERVER_LOG; then - echo -e "Found \"$EX\"" >> $CLIENT_LOG - EX_FOUND=1 - else - echo -e "Not found \"$EX\"" >> $CLIENT_LOG - fi - if [ "$EX_FOUND" == "0" ]; then - echo -e "*** FAILED: model_metrics/$TARGET" >> $CLIENT_LOG - RET=1 - fi - fi -done - if [ $RET -eq 0 ]; then echo -e "\n***\n*** Test Passed\n***" else diff --git a/qa/L0_openai/generate_engine.py b/qa/L0_openai/generate_engine.py deleted file mode 100644 index 22c21d2209..0000000000 --- a/qa/L0_openai/generate_engine.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright (c) 2025-2026, NVIDIA CORPORATION. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from argparse import ArgumentParser - -from tensorrt_llm import BuildConfig -from tensorrt_llm._tensorrt_engine import LLM -from tensorrt_llm.lora_manager import LoraConfig -from tensorrt_llm.plugin import PluginConfig - - -def generate_model_engine(model: str, engines_path: str): - config = BuildConfig(plugin_config=PluginConfig(gemm_plugin="auto")) - - lora_config = LoraConfig( - lora_target_modules=["attn_q", "attn_k", "attn_v"], - max_lora_rank=8, - max_loras=4, - max_cpu_loras=8, - ) - - engine = LLM( - model, - dtype="float16", - max_batch_size=128, - build_config=config, - guided_decoding_backend="xgrammar", - lora_config=lora_config, - ) - - engine.save(engines_path) - engine.shutdown() - - -if __name__ == "__main__": - parser = ArgumentParser() - parser.add_argument( - "--model", "-m", help="model huggingface id or path to the model" - ) - parser.add_argument("--engine_path", "-e", help="directory of the output engine") - FLAGS = parser.parse_args() - - generate_model_engine(FLAGS.model, FLAGS.engine_path) - print(f"model {FLAGS.model}'s engine has been saved to {FLAGS.engine_path}") diff --git a/qa/L0_openai/test.sh b/qa/L0_openai/test.sh index 5fd4daf239..0921bce98e 100755 --- a/qa/L0_openai/test.sh +++ b/qa/L0_openai/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -27,16 +27,6 @@ ### Helpers ### -function download_tensorrt_llm_models { - TENSORRTLLM_DIR="$1" - rm -rf ${TENSORRTLLM_DIR} && mkdir ${TENSORRTLLM_DIR} - git clone --filter=blob:none --no-checkout https://github.com/triton-inference-server/TensorRT-LLM.git ${TENSORRTLLM_DIR} - pushd ${TENSORRTLLM_DIR} - git sparse-checkout set triton_backend/all_models - git checkout ${TENSORRTLLM_REPO_TAG} - popd -} - function install_deps() { # Install python bindings for tritonserver and tritonfrontend # pip install /opt/tritonserver/python/triton*.whl @@ -48,12 +38,7 @@ function install_deps() { pip install -r requirements-test.txt if [ "${IMAGE_KIND}" == "TRTLLM" ]; then - # TODO: Remove this when the next stable version of TRT-LLM is available - TENSORRTLLM_DIR="/workspace/TensorRT-LLM" - download_tensorrt_llm_models ${TENSORRTLLM_DIR} - - prepare_tensorrtllm meta-llama/Meta-Llama-3.1-8B-Instruct tests/tensorrtllm_models /tmp/engines/llama/3.1-8b-instruct/ ${TENSORRTLLM_DIR} - prepare_tensorrtllm mistralai/Mistral-Nemo-Instruct-2407 tests/tensorrtllm_mistral_models /tmp/engines/mistral/nemo-instruct-2407/ ${TENSORRTLLM_DIR} + prepare_tensorrtllm else prepare_vllm fi @@ -68,46 +53,38 @@ function prepare_tensorrtllm() { # FIXME: Remove when testing TRT-LLM containers built from source pip install -r requirements.txt - MODEL="$1" - MODEL_REPO="$2" - ENGINE_PATH="$3" - TENSORRTLLM_DIR="$4" - TRITON_BACKEND=tensorrtllm - XGRAMMAR_TOKENIZER_INFO_PATH=tokenizer_info/${MODEL}/xgrammar_tokenizer_info.json - GUIDED_DECODING_BACKEND=xgrammar - + MODEL="meta-llama/Meta-Llama-3.1-8B-Instruct" + MODEL_REPO="tests/tensorrtllm_models" mkdir -p ${MODEL_REPO} - cp ${TENSORRTLLM_DIR}/triton_backend/all_models/inflight_batcher_llm/* "${MODEL_REPO}" -r + cp /app/all_models/inflight_batcher_llm/* "${MODEL_REPO}" -r # Ensemble model is not needed for the test rm -rf ${MODEL_REPO}/ensemble - # 1. Generate the model's trt engines - python3 ../generate_engine.py --model "${MODEL}" --engine_path "${ENGINE_PATH}" + # 1. Download model from HF + huggingface-cli download ${MODEL} - # 2. Generate the model's xgrammar tokenizer info. In order to run on C++ backend, we need an extra step to extract tokenizer’s information into json format. - XGRAMMAR_TOKENIZER_INFO_DIR=tokenizer_info/${MODEL} - rm -rf ${XGRAMMAR_TOKENIZER_INFO_DIR} - python3 /app/examples/generate_xgrammar_tokenizer_info.py --model_dir ${MODEL} --output_dir ${XGRAMMAR_TOKENIZER_INFO_DIR} + HF_LLAMA_MODEL=`python3 -c "from pathlib import Path; from huggingface_hub import hf_hub_download; print(Path(hf_hub_download('${MODEL}', filename='config.json')).parent)"` + CKPT_PATH=/tmp/ckpt/llama/3.1-8b-instruct/ + ENGINE_PATH=/tmp/engines/llama/3.1-8b-instruct/ - # 3. Prepare model repository + # 2. Convert weights + python3 /app/examples/llama/convert_checkpoint.py --model_dir ${HF_LLAMA_MODEL} \ + --output_dir ${CKPT_PATH} \ + --dtype float16 + + # 3. Build engine + # max_batch_size set to 128 to avoid OOM errors + trtllm-build --checkpoint_dir ${CKPT_PATH} \ + --gemm_plugin auto \ + --max_batch_size 128 \ + --output_dir ${ENGINE_PATH} + + # 4. Prepare model repository FILL_TEMPLATE="/app/tools/fill_template.py" - python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/preprocessing/config.pbtxt tokenizer_dir:${ENGINE_PATH},triton_max_batch_size:64,preprocessing_instance_count:1,max_queue_size:0 - python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/postprocessing/config.pbtxt tokenizer_dir:${ENGINE_PATH},triton_max_batch_size:64,postprocessing_instance_count:1 - python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/tensorrt_llm_bls/config.pbtxt triton_max_batch_size:64,decoupled_mode:True,bls_instance_count:1,accumulate_tokens:False,logits_datatype:TYPE_FP32,prompt_embedding_table_data_type:TYPE_FP16 - python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/tensorrt_llm/config.pbtxt triton_backend:${TRITON_BACKEND},triton_max_batch_size:64,decoupled_mode:True,max_beam_width:1,engine_dir:${ENGINE_PATH},batching_strategy:inflight_fused_batching,max_queue_size:0,max_queue_delay_microseconds:1000,encoder_input_features_data_type:TYPE_FP16,logits_datatype:TYPE_FP32,exclude_input_in_output:True,prompt_embedding_table_data_type:TYPE_FP16,guided_decoding_backend:${GUIDED_DECODING_BACKEND},xgrammar_tokenizer_info_path:${XGRAMMAR_TOKENIZER_INFO_PATH} - - # 4. Prepare lora adapters - # FIXME: Remove this WAR when it is fixed in the future stable version of TRT-LLM. - sed -i 's/dims: \[ -1, 3 \]/dims: \[ -1, 4 \]/' ${MODEL_REPO}/tensorrt_llm/config.pbtxt - sed -i 's/dims: \[ -1, 3 \]/dims: \[ -1, 4 \]/' ${MODEL_REPO}/tensorrt_llm_bls/config.pbtxt - pushd ${MODEL_REPO}/tensorrt_llm_bls/1 - for lora_name in silk-road/luotuo-lora-7b-0.1 kunishou/Japanese-Alpaca-LoRA-7b-v0; do - name=$(basename $lora_name) - git clone https://huggingface.co/$lora_name - python3 /app/examples/hf_lora_convert.py -i $name -o $name-weights --storage-type float16 - rm -rf $name - done - popd + python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/preprocessing/config.pbtxt tokenizer_dir:${HF_LLAMA_MODEL},triton_max_batch_size:64,preprocessing_instance_count:1,max_queue_size:0 + python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/postprocessing/config.pbtxt tokenizer_dir:${HF_LLAMA_MODEL},triton_max_batch_size:64,postprocessing_instance_count:1 + python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/tensorrt_llm_bls/config.pbtxt triton_max_batch_size:64,decoupled_mode:True,bls_instance_count:1,accumulate_tokens:False,logits_datatype:TYPE_FP32 + python3 ${FILL_TEMPLATE} -i ${MODEL_REPO}/tensorrt_llm/config.pbtxt triton_backend:tensorrtllm,triton_max_batch_size:64,decoupled_mode:True,max_beam_width:1,engine_dir:${ENGINE_PATH},batching_strategy:inflight_fused_batching,max_queue_size:0,max_queue_delay_microseconds:1000,encoder_input_features_data_type:TYPE_FP16,logits_datatype:TYPE_FP32,exclude_input_in_output:True } function pre_test() { @@ -123,30 +100,17 @@ function pre_test() { function run_test() { pushd openai/ TEST_LOG="test_openai.log" - TEST_XML="test_openai.xml" - TEST_LOG_MISTRAL="test_openai_mistral.log" - TEST_XML_MISTRAL="test_openai_mistral.xml" + # Capture error code without exiting to allow log collection set +e - pytest -s -v --junitxml=${TEST_XML} tests/ 2>&1 | tee ${TEST_LOG} - if [ ${PIPESTATUS[0]} -ne 0 ]; then + pytest -s -v --junitxml=test_openai.xml tests/ 2>&1 > ${TEST_LOG} + if [ $? -ne 0 ]; then + cat ${TEST_LOG} echo -e "\n***\n*** Test Failed\n***" RET=1 fi set -e - if [ "$RET" == "0" ]; then - # rerun the tool calling tests with mistral model to cover the mistral tool call parser - set +e - TEST_TOOL_CALL_PARSER="mistral" TEST_TOKENIZER="mistralai/Mistral-Nemo-Instruct-2407" \ - pytest -s -v --junitxml=${TEST_XML_MISTRAL} tests/test_tool_calling.py 2>&1 | tee ${TEST_LOG_MISTRAL} - if [ ${PIPESTATUS[0]} -ne 0 ]; then - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - set -e - fi - # Collect logs for error analysis when needed cp *.xml *.log ../../../ popd diff --git a/qa/L0_orca/test.sh b/qa/L0_orca/test.sh index 50b7f450fb..6069a75048 100755 --- a/qa/L0_orca/test.sh +++ b/qa/L0_orca/test.sh @@ -36,7 +36,7 @@ MODEL_NAME="gpt2_tensorrt_llm" NAME="tensorrt_llm_benchmarking_test" MODEL_REPOSITORY="$(pwd)/triton_model_repo" TENSORRTLLM_BACKEND_DIR="/workspace/tensorrtllm_backend" -GPT_DIR="$TENSORRTLLM_BACKEND_DIR/tensorrt_llm/examples/models/core/gpt" +GPT_DIR="$TENSORRTLLM_BACKEND_DIR/tensorrt_llm/examples/gpt" TOKENIZER_DIR="$GPT_DIR/gpt2" ENGINES_DIR="${BASE_DIR}/engines/inflight_batcher_llm/${NUM_GPUS}-gpu" TRITON_DIR=${TRITON_DIR:="/opt/tritonserver"} @@ -46,7 +46,97 @@ SERVER_LOG="${NAME}_server.log" SERVER_TIMEOUT=${SERVER_TIMEOUT:=120} CLIENT_PY=${BASE_DIR}/orca_http_test.py CLIENT_LOG="${NAME}_orca_http_test.log" -source ../common/trtllm_util.sh +source ../common/util.sh + +function prepare_model_repository { + rm -rf ${MODEL_REPOSITORY} && mkdir ${MODEL_REPOSITORY} + cp -r ${TENSORRTLLM_BACKEND_DIR}/all_models/inflight_batcher_llm/* ${MODEL_REPOSITORY} + rm -rf ${MODEL_REPOSITORY}/tensorrt_llm_bls + mv "${MODEL_REPOSITORY}/ensemble" "${MODEL_REPOSITORY}/${MODEL_NAME}" + + replace_config_tags "model_version: -1" "model_version: 1" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" + replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" + replace_config_tags 'name: "ensemble"' "name: \"$MODEL_NAME\"" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" + replace_config_tags '${logits_datatype}' "TYPE_FP32" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" + + replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" + replace_config_tags '${preprocessing_instance_count}' '1' "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" + replace_config_tags '${tokenizer_dir}' "${TOKENIZER_DIR}/" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" + replace_config_tags '${logits_datatype}' "TYPE_FP32" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" + replace_config_tags '${max_queue_delay_microseconds}' "1000000" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" + replace_config_tags '${max_queue_size}' "0" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" + + replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" + replace_config_tags '${postprocessing_instance_count}' '1' "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" + replace_config_tags '${tokenizer_dir}' "${TOKENIZER_DIR}/" "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" + replace_config_tags '${logits_datatype}' "TYPE_FP32" "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" + + replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${decoupled_mode}' 'true' "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${max_queue_delay_microseconds}' "1000000" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${batching_strategy}' 'inflight_fused_batching' "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${engine_dir}' "${ENGINES_DIR}" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${triton_backend}' "tensorrtllm" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${max_queue_size}' "0" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${logits_datatype}' "TYPE_FP32" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${encoder_input_features_data_type}' "TYPE_FP32" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" +} + +# Wait until server health endpoint shows ready. Sets WAIT_RET to 0 on +# success, 1 on failure +function wait_for_server_ready() { + local wait_time_secs="${1:-30}" + shift + local spids=("$@") + + WAIT_RET=0 + + for _ in $(seq "$wait_time_secs"); do + for pid in "${spids[@]}"; do + if ! kill -0 "$pid" >/dev/null 2>&1; then + echo "=== Server not running." + WAIT_RET=1 + return + fi + done + + sleep 1 + + if curl -s --fail localhost:8000/v2/health/ready && + curl -s --fail -w "%{http_code}" -o /dev/null -d '{"log_verbose_level":1}' localhost:8000/v2/logging; then + return + fi + done + + echo "=== Timeout $wait_time_secs secs. Server not ready." + WAIT_RET=1 +} + +function run_server { + python3 ${TENSORRTLLM_BACKEND_DIR}/scripts/launch_triton_server.py --world_size="${NUM_GPUS}" --model_repo="${MODEL_REPOSITORY}" >${SERVER_LOG} 2>&1 & + sleep 2 # allow time to obtain the pid(s) + # Read PIDs into an array, trimming whitespaces + readarray -t SERVER_PID < <(pgrep "tritonserver") + + wait_for_server_ready ${SERVER_TIMEOUT} "${SERVER_PID[@]}" + if [ "$WAIT_RET" != "0" ]; then + # Cleanup + kill "${SERVER_PID[@]}" >/dev/null 2>&1 || true + echo -e "\n***\n*** Failed to start $SERVER\n***" + cat $SERVER_LOG + exit 1 + fi +} + +function kill_server { + pgrep tritonserver | xargs kill -SIGINT + for pid in "${SERVER_PID[@]}"; do + echo "Waiting for proc ${pid} to terminate..." + while kill -0 $pid >/dev/null 2>&1; do + sleep 1 + done + done +} clone_tensorrt_llm_backend_repo build_gpt2_base_model diff --git a/qa/L0_parameters/class_count_test.py b/qa/L0_parameters/class_count_test.py deleted file mode 100755 index 801c442b87..0000000000 --- a/qa/L0_parameters/class_count_test.py +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import sys - -sys.path.append("../common") - -import os -import unittest - -import numpy as np -import test_util as tu -import tritonclient.grpc as grpcclient -import tritonclient.http as httpclient -from tritonclient.utils import InferenceServerException - - -class ClassificationParameterTest(tu.TestResultCollector): - def setUp(self): - self.protocol = os.environ.get("CLIENT_TYPE", "http") - if self.protocol == "http": - self.client = httpclient.InferenceServerClient("localhost:8000") - else: - self.client = grpcclient.InferenceServerClient("localhost:8001") - - def _prepare_io(self, input_data, dtype): - if self.protocol == "http": - inputs = [httpclient.InferInput("INPUT0", input_data.shape, dtype)] - outputs = [httpclient.InferRequestedOutput(name="OUTPUT0", class_count=5)] - else: - inputs = [grpcclient.InferInput("INPUT0", input_data.shape, dtype)] - outputs = [grpcclient.InferRequestedOutput(name="OUTPUT0", class_count=5)] - inputs[0].set_data_from_numpy(input_data) - return inputs, outputs - - def test_classificattion(self): - shape = (1, 8) - dtype = "FP32" - model_name = "identity_fp32" - input_data = np.ones(shape, dtype=np.float32) - - inputs, outputs = self._prepare_io(input_data, dtype) - result = self.client.infer( - model_name=model_name, inputs=inputs, outputs=outputs - ) - output = result.get_output("OUTPUT0") - if self.protocol == "http": - output_dtype = output["datatype"] - else: - output_dtype = output.datatype - - self.assertEqual(output_dtype, "BYTES") - - # Validate shape matches to the class_count - output_data = result.as_numpy("OUTPUT0") - self.assertIsNotNone(output_data) - self.assertEqual(output_data.shape, (1, 5)) - - for res_str_bytes in np.nditer(output_data, flags=["refs_ok"]): - res_str = res_str_bytes.item().decode("utf-8") - self.assertTrue(res_str.startswith("1.000000:")) - - def test_classificattion_unsupported_data_type(self): - shape = (1, 8) - model_name = "identity_bytes" - dtype = "BYTES" - input_data = np.array([["test"] * shape[1]], dtype=object) - - inputs, outputs = self._prepare_io(input_data, dtype) - with self.assertRaises(InferenceServerException) as e: - self.client.infer(model_name=model_name, inputs=inputs, outputs=outputs) - - self.assertIn( - "class result not available for output due to unsupported type 'BYTES'", - str(e.exception), - ) - - def test_classification_output_tensor_too_large(self): - max_elements = 1_000_000 - shape = (1, max_elements + 1) - dtype = "FP32" - model_name = "identity_fp32" - input_data = np.ones(shape, dtype=np.float32) - - inputs, outputs = self._prepare_io(input_data, dtype) - with self.assertRaises(InferenceServerException) as e: - self.client.infer(model_name=model_name, inputs=inputs, outputs=outputs) - - self.assertIn("classification output tensor too large", str(e.exception)) - - -if __name__ == "__main__": - unittest.main() diff --git a/qa/L0_parameters/parameters_test.py b/qa/L0_parameters/parameters_test.py index 90221c12d7..a20d13c1eb 100755 --- a/qa/L0_parameters/parameters_test.py +++ b/qa/L0_parameters/parameters_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -30,86 +30,66 @@ sys.path.append("../common") +import os import queue import unittest from functools import partial +from unittest import IsolatedAsyncioTestCase import numpy as np -import requests import tritonclient.grpc as grpcclient import tritonclient.grpc.aio as asyncgrpcclient import tritonclient.http as httpclient import tritonclient.http.aio as asynchttpclient from tritonclient.utils import InferenceServerException -_TRITON_RESERVED_CLIENT_ERROR = "is a reserved parameter and cannot be specified" -_TRITON_RESERVED_SERVER_ERROR = "reserved for Triton usage" - -# docs/protocol/extension_parameters.md — reserved names -_RESERVED_PARAMETER_KEYS = ( - "sequence_id", - "sequence_start", - "sequence_end", - "priority", - "timeout", - "headers", - "binary_data_output", - "triton_enable_empty_final_response", - "triton_final_response", - "triton_injected_via_header", -) - -_HTTP_LOCALHOST = "http://localhost:8000" - - -def _infer_url(model_name="parameter"): - return f"{_HTTP_LOCALHOST}/v2/models/{model_name}/infer" - - -def _minimal_fp32_infer_body(parameters=None): - """KServe HTTP infer JSON matching qa/L0_parameters model `parameter` (INPUT0 FP32 [1]).""" - body = { - "inputs": [ - { - "name": "INPUT0", - "shape": [1], - "datatype": "FP32", - "data": [1.0], - } - ], - } - if parameters is not None: - body["parameters"] = parameters - return body - +TEST_HEADER = os.environ.get("TEST_HEADER") -_FORWARD_HEADERS = { - "header_1": "value_1", - "header_2": "value_2", - "my_header_1": "my_value_1", - "my_header_2": "my_value_2", - "my_header_3": 'This is a "quoted" string with a backslash\\ ', -} +class InferenceParametersTest(IsolatedAsyncioTestCase): + async def asyncSetUp(self): + self.http = httpclient.InferenceServerClient(url="localhost:8000") + self.async_http = asynchttpclient.InferenceServerClient(url="localhost:8000") + self.grpc = grpcclient.InferenceServerClient(url="localhost:8001") + self.async_grpc = asyncgrpcclient.InferenceServerClient(url="localhost:8001") + + self.parameter_list = [] + self.parameter_list.append({"key1": "value1", "key2": "value2"}) + self.parameter_list.append({"key1": 1, "key2": 2}) + self.parameter_list.append({"key1": 123.123, "key2": 321.321}) + self.parameter_list.append({"key1": True, "key2": "value2"}) + self.parameter_list.append({"triton_": True, "key2": "value2"}) + + # Only "test_params" tests parameters without headers. + if TEST_HEADER != "test_params": + self.headers = { + "header_1": "value_1", + "header_2": "value_2", + "my_header_1": "my_value_1", + "my_header_2": "my_value_2", + "my_header_3": 'This is a "quoted" string with a backslash\ ', + } -def _grpc_stream_callback(user_data, result, error): - if error: - user_data.put(error) - else: - user_data.put(result) + # only these headers should be forwarded to the model. + if TEST_HEADER == "test_grpc_header_forward_pattern_case_sensitive": + self.expected_headers = {} + else: + self.expected_headers = { + "my_header_1": "my_value_1", + "my_header_2": "my_value_2", + "my_header_3": 'This is a "quoted" string with a backslash\ ', + } + else: + self.headers = {} + self.expected_headers = {} + def callback(user_data, result, error): + if error: + user_data.put(error) + else: + user_data.put(result) -class InferenceParametersTest(unittest.IsolatedAsyncioTestCase): - async def asyncSetUp(self): - self.httpclient = httpclient.InferenceServerClient(url="localhost:8000") - self.async_httpclient = asynchttpclient.InferenceServerClient( - url="localhost:8000" - ) - self.grpcclient = grpcclient.InferenceServerClient(url="localhost:8001") - self.async_grpcclient = asyncgrpcclient.InferenceServerClient( - url="localhost:8001" - ) - self.grpcclient_callback = _grpc_stream_callback + self.grpc_callback = callback def create_inputs(self, client_type): inputs = [] @@ -120,257 +100,128 @@ def create_inputs(self, client_type): return inputs async def send_request_and_verify( - self, - client_type, - client, - parameters, - headers, - expected_headers, - is_async_infer=False, - model_name="parameter", + self, client_type, client, is_async=False, model_name="parameter" ): inputs = self.create_inputs(client_type) - - if is_async_infer: - if client_type == httpclient: - result = client.async_infer( - model_name=model_name, - inputs=inputs, - parameters=parameters, - headers=headers, - ).get_result() - elif client_type == grpcclient: - user_data = queue.Queue() - client.async_infer( - model_name=model_name, - inputs=inputs, - parameters=parameters, - headers=headers, - callback=partial(self.grpcclient_callback, user_data), - ) - result = user_data.get() - self.assertIsNot(result, InferenceServerException) - else: - raise ValueError(f"Unsupported client type: {client_type}") - else: + for parameters in self.parameter_list: + # Setup infer callable to re-use below for brevity infer_callable = partial( client.infer, model_name=model_name, inputs=inputs, parameters=parameters, - headers=headers, + headers=self.headers, ) - if client_type == asynchttpclient or client_type == asyncgrpcclient: - result = await infer_callable() + + # The `triton_` prefix is reserved for Triton usage + should_error = False + if "triton_" in parameters.keys(): + should_error = True + + if is_async: + if should_error: + with self.assertRaises(InferenceServerException): + await infer_callable() + return + else: + result = await infer_callable() else: - result = infer_callable() + if should_error: + with self.assertRaises(InferenceServerException): + infer_callable() + return + else: + result = infer_callable() - self.verify_outputs(result, parameters, expected_headers) + self.verify_outputs(result, parameters) - def verify_outputs(self, result, parameters, expected_headers): + def verify_outputs(self, result, parameters): keys = result.as_numpy("key") values = result.as_numpy("value") keys = keys.astype(str).tolist() - expected_keys = list(parameters.keys()) + list(expected_headers.keys()) - self.assertEqual( - set(keys), - set(expected_keys), - msg=f"keys: {keys}, expected_keys: {expected_keys}", - ) + expected_keys = list(parameters.keys()) + list(self.expected_headers.keys()) + self.assertEqual(set(keys), set(expected_keys)) # We have to convert the parameter values to string expected_values = [] for expected_value in list(parameters.values()): expected_values.append(str(expected_value)) - for value in expected_headers.values(): + for value in self.expected_headers.values(): expected_values.append(value) - self.assertEqual( - set(values.astype(str).tolist()), - set(expected_values), - msg=f"values: {values.astype(str).tolist()}, expected_values: {expected_values}", - ) + self.assertEqual(set(values.astype(str).tolist()), set(expected_values)) - async def _verify_grpc_stream_infer(self, parameters, headers, expected_headers): - user_data = queue.Queue() - self.grpcclient.start_stream( - callback=partial(self.grpcclient_callback, user_data), headers=headers - ) - inputs = self.create_inputs(grpcclient) - self.grpcclient.async_stream_infer( - model_name="parameter", inputs=inputs, parameters=parameters - ) - result = user_data.get() - self.assertIsNot(result, InferenceServerException) - self.verify_outputs(result, parameters, expected_headers) - self.grpcclient.stop_stream() - - def _raw_http_post_infer(self, body_dict, extra_headers=None): - """POST /v2/models/.../infer without tritonclient (no header normalization).""" - headers = {"Content-Type": "application/json"} - if extra_headers: - headers.update(extra_headers) - return requests.post( - _infer_url("parameter"), - json=body_dict, - headers=headers, - timeout=60, - ) + async def test_grpc_parameter(self): + await self.send_request_and_verify(grpcclient, self.grpc) - def _assert_raw_http_400(self, response, text_substr): - """Assert status 400 and body contains `text_substr`.""" - snippet = response.text[:2000] if response.text else "" - self.assertEqual(response.status_code, 400, msg=snippet) - self.assertIn(text_substr, response.text, msg=snippet) + async def test_http_parameter(self): + await self.send_request_and_verify(httpclient, self.http) - async def _run_client_infer_suite(self, parameters, headers, expected_headers): - """ - Full client matrix: gRPC/HTTP sync+async, stream, ensemble. - """ - await self.send_request_and_verify( - grpcclient, self.grpcclient, parameters, headers.copy(), expected_headers - ) + async def test_async_http_parameter(self): await self.send_request_and_verify( - httpclient, self.httpclient, parameters, headers.copy(), expected_headers - ) - await self.send_request_and_verify( - asynchttpclient, - self.async_httpclient, - parameters, - headers.copy(), - expected_headers, - ) - await self.send_request_and_verify( - asyncgrpcclient, - self.async_grpcclient, - parameters, - headers.copy(), - expected_headers, - ) - await self.send_request_and_verify( - httpclient, - self.httpclient, - parameters, - headers.copy(), - expected_headers, - is_async_infer=True, - ) - await self.send_request_and_verify( - grpcclient, - self.grpcclient, - parameters, - headers.copy(), - expected_headers, - is_async_infer=True, - ) - await self._verify_grpc_stream_infer( - parameters, headers.copy(), expected_headers + asynchttpclient, self.async_http, is_async=True ) + + async def test_async_grpc_parameter(self): await self.send_request_and_verify( - httpclient, - self.httpclient, - parameters, - headers.copy(), - expected_headers, - model_name="ensemble", + asyncgrpcclient, self.async_grpc, is_async=True ) - async def test_params(self): - for parameters in [ - {"key1": "value1", "key2": "value2"}, - {"key1": 1, "key2": 2}, - {"key1": 123.123, "key2": 321.321}, - {"key1": True, "key2": "value2"}, - ]: - await self._run_client_infer_suite(parameters, {}, {}) - - async def test_params_reserved_rejected(self): - for reserved_key in _RESERVED_PARAMETER_KEYS: - parameters = {reserved_key: "dummy-value"} - body = _minimal_fp32_infer_body(parameters) - - # Raw HTTP - if reserved_key.startswith("triton_"): - r = self._raw_http_post_infer(body) - self._assert_raw_http_400(r, _TRITON_RESERVED_SERVER_ERROR) - - # Python clients - for client_type, client in ( - (httpclient, self.httpclient), - (asynchttpclient, self.async_httpclient), - (grpcclient, self.grpcclient), - (asyncgrpcclient, self.async_grpcclient), - ): - inputs = self.create_inputs(client_type) - with self.assertRaises(InferenceServerException) as cm: - if client_type in (asynchttpclient, asyncgrpcclient): - await client.infer( - model_name="parameter", - inputs=inputs, - parameters=parameters, - ) - else: - client.infer( - model_name="parameter", - inputs=inputs, - parameters=parameters, - ) - msg = str(cm.exception) - # Reserved parameters are rejected by the client - self.assertIn(_TRITON_RESERVED_CLIENT_ERROR, msg, msg=msg) - - async def test_headers(self): - expected_headers = { - "my_header_1": "my_value_1", - "my_header_2": "my_value_2", - "my_header_3": 'This is a "quoted" string with a backslash\\ ', - } - await self._run_client_infer_suite({}, _FORWARD_HEADERS, expected_headers) - - async def test_grpc_header_forward_pattern_case_sensitive(self): - expected_headers = {} - await self._run_client_infer_suite({}, _FORWARD_HEADERS, expected_headers) - - async def test_headers_reserved_rejected(self): - body = _minimal_fp32_infer_body({}) - for reserved_key in _RESERVED_PARAMETER_KEYS: - # Raw HTTP - r = self._raw_http_post_infer( - body, extra_headers={reserved_key: "dummy-value"} + def test_http_async_parameter(self): + inputs = self.create_inputs(httpclient) + # Skip the parameter that returns an error + parameter_list = self.parameter_list[:-1] + for parameters in parameter_list: + result = self.http.async_infer( + model_name="parameter", + inputs=inputs, + parameters=parameters, + headers=self.headers, + ).get_result() + self.verify_outputs(result, parameters) + + def test_grpc_async_parameter(self): + user_data = queue.Queue() + inputs = self.create_inputs(grpcclient) + # Skip the parameter that returns an error + parameter_list = self.parameter_list[:-1] + for parameters in parameter_list: + self.grpc.async_infer( + model_name="parameter", + inputs=inputs, + parameters=parameters, + headers=self.headers, + callback=partial(self.grpc_callback, user_data), ) - self._assert_raw_http_400(r, _TRITON_RESERVED_SERVER_ERROR) - - # Python clients - for client_type, client in ( - (httpclient, self.httpclient), - (asynchttpclient, self.async_httpclient), - (grpcclient, self.grpcclient), - (asyncgrpcclient, self.async_grpcclient), - ): - inputs = self.create_inputs(client_type) - with self.assertRaises(InferenceServerException) as cm: - if client_type in (asynchttpclient, asyncgrpcclient): - await client.infer( - model_name="parameter", - inputs=inputs, - parameters={}, - headers={reserved_key: "dummy-value"}, - ) - else: - client.infer( - model_name="parameter", - inputs=inputs, - parameters={}, - headers={reserved_key: "dummy-value"}, - ) - msg = str(cm.exception) - # Headers are not rejected by the client - self.assertIn(_TRITON_RESERVED_SERVER_ERROR, msg, msg=msg) + result = user_data.get() + self.assertFalse(result is InferenceServerException) + self.verify_outputs(result, parameters) + + def test_grpc_stream_parameter(self): + user_data = queue.Queue() + self.grpc.start_stream( + callback=partial(self.grpc_callback, user_data), headers=self.headers + ) + inputs = self.create_inputs(grpcclient) + # Skip the parameter that returns an error + parameter_list = self.parameter_list[:-1] + for parameters in parameter_list: + # async stream infer + self.grpc.async_stream_infer( + model_name="parameter", inputs=inputs, parameters=parameters + ) + result = user_data.get() + self.assertFalse(result is InferenceServerException) + self.verify_outputs(result, parameters) + self.grpc.stop_stream() + + async def test_ensemble_parameter_forwarding(self): + await self.send_request_and_verify(httpclient, self.http, model_name="ensemble") async def asyncTearDown(self): - self.httpclient.close() - self.grpcclient.close() - await self.async_grpcclient.close() - await self.async_httpclient.close() + self.http.close() + self.grpc.close() + await self.async_grpc.close() + await self.async_http.close() if __name__ == "__main__": diff --git a/qa/L0_parameters/test.sh b/qa/L0_parameters/test.sh index a6ca428c4b..c53b02d4b7 100755 --- a/qa/L0_parameters/test.sh +++ b/qa/L0_parameters/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023-2026, NVIDIA CORPORATION. All rights reserved. +# Copyright 2023-2024, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -39,6 +39,8 @@ if [ ! -z "$TEST_REPO_ARCH" ]; then fi CLIENT_LOG="./client.log" +TEST_SCRIPT_PY="parameters_test.py" + SERVER=/opt/tritonserver/bin/tritonserver SERVER_LOG="./inference_server.log" source ../common/util.sh @@ -52,27 +54,27 @@ mkdir -p "${MODELDIR}/ensemble/1" # https://jirasw.nvidia.com/browse/DLIS-4673 all_tests=("test_params" - "test_params_reserved_rejected" "test_headers" - "test_grpc_header_forward_pattern_case_sensitive" - "test_headers_reserved_rejected") + "test_header_forward_pattern_case_insensitive" + "test_grpc_header_forward_pattern_case_sensitive") RET=0 -for test in "${all_tests[@]}"; do +for i in "${all_tests[@]}"; do + # TEST_HEADER is a parameter used by `parameters_test.py` that controls + # whether the script will test for inclusion of headers in parameters or not. SERVER_ARGS="--model-repository=${MODELDIR} --exit-timeout-secs=120" - if [ "$test" == "test_headers" ]; then - # gRPC lowercases metadata keys; HTTP may lowercase too — match prefix case-insensitively. + if [ "$i" == "test_headers" ]; then + SERVER_ARGS+=" --grpc-header-forward-pattern my_header.*" + SERVER_ARGS+=" --http-header-forward-pattern my_header.*" + elif [ "$i" == "test_header_forward_pattern_case_insensitive" ]; then SERVER_ARGS+=" --grpc-header-forward-pattern MY_HEADER.*" SERVER_ARGS+=" --http-header-forward-pattern MY_HEADER.*" # NOTE: headers sent through the python HTTP client may be automatically # lowercased by internal libraries like geventhttpclient, so we only test # GRPC client for case-sensitivity here: # https://github.com/geventhttpclient/geventhttpclient/blob/d1e14356c3b02099c879cf9b3bdb684a0cbd8bf5/src/geventhttpclient/header.py#L62-L63 - elif [ "$test" == "test_grpc_header_forward_pattern_case_sensitive" ]; then + elif [ "$i" == "test_grpc_header_forward_pattern_case_sensitive" ]; then SERVER_ARGS+=" --grpc-header-forward-pattern (?-i)MY_HEADER.*" - elif [ "$test" == "test_headers_reserved_rejected" ]; then - SERVER_ARGS+=" --grpc-header-forward-pattern .*" - SERVER_ARGS+=" --http-header-forward-pattern .*" fi run_server if [ "$SERVER_PID" == "0" ]; then @@ -82,7 +84,7 @@ for test in "${all_tests[@]}"; do fi set +e - python3 -m unittest "parameters_test.InferenceParametersTest.${test}" >$CLIENT_LOG 2>&1 + TEST_HEADER="$i" python3 $TEST_SCRIPT_PY >$CLIENT_LOG 2>&1 if [ $? -ne 0 ]; then cat $CLIENT_LOG echo -e "\n***\n*** Test Failed\n***" @@ -95,65 +97,10 @@ for test in "${all_tests[@]}"; do wait $SERVER_PID done - -# Test Classification Extension -PYTHON_MODELS_DIR="${PYTHON_MODELS_DIR:-/opt/tritonserver/qa/python_models}" -MODELDIR="models" -TEST_RESULT_FILE="test_results.txt" -TEST_SCRIPT_PY="./class_count_test.py" - -rm -rf $MODELDIR -mkdir -p "${MODELDIR}/identity_fp32/1" -cp ${PYTHON_MODELS_DIR}/identity_fp32/config.pbtxt "${MODELDIR}/identity_fp32/" -cp ${PYTHON_MODELS_DIR}/identity_fp32/model.py "${MODELDIR}/identity_fp32/1/" - -mkdir -p "${MODELDIR}/identity_bytes/1" -cp ${PYTHON_MODELS_DIR}/identity_fp32/config.pbtxt "${MODELDIR}/identity_bytes/" -cp ${PYTHON_MODELS_DIR}/identity_fp32/model.py "${MODELDIR}/identity_bytes/1/" -(cd "${MODELDIR}/identity_bytes" && \ - sed -i 's/identity_fp32/identity_bytes/' config.pbtxt && \ - sed -i 's/TYPE_FP32/TYPE_STRING/' config.pbtxt ) - -SERVER_ARGS="--model-repository=`pwd`/${MODELDIR} --log-verbose=1" -for client_type in http grpc; do - export CLIENT_TYPE=$client_type - SERVER_LOG="./class_count_test_${client_type}_server.log" - CLIENT_LOG="./class_count_test_${client_type}_client.log" - rm -f $SERVER_LOG $CLIENT_LOG - run_server - if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 - fi - - set +e - python3 $TEST_SCRIPT_PY -v >>"$CLIENT_LOG" 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed - class_count_${client_type}_test_client\n***" - RET=1 - else - check_test_results $TEST_RESULT_FILE 3 - if [ $? -ne 0 ]; then - cat $TEST_RESULT_FILE - echo -e "\n***\n*** Test Result Verification Failed - class_count_${client_type}_test_client\n***" - RET=1 - fi - fi - kill $SERVER_PID - wait $SERVER_PID - - if [ $? -ne 0 ]; then - echo -e "\n***\n*** Test Server shut down non-gracefully\n***" - RET=1 - fi - set -e -done - if [ $RET -eq 0 ]; then echo -e "\n***\n*** Test Passed\n***" else + cat $CLIENT_LOG echo -e "\n***\n*** Test FAILED\n***" fi diff --git a/qa/L0_passive_instance/test.sh b/qa/L0_passive_instance/test.sh index be56bb4185..8948434485 100755 --- a/qa/L0_passive_instance/test.sh +++ b/qa/L0_passive_instance/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2021-2025, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -45,8 +45,7 @@ CLIENT_LOG="./client.log" TEST_SCRIPT_PY=passive_instance_test.py EXPECTED_NUM_TESTS="1" -pip3 install perf_analyzer -PERF_ANALYZER=perf_analyzer +PERF_ANALYZER=../clients/perf_analyzer MODEL=distributed_int32_int32_int32 SERVER=/opt/tritonserver/bin/tritonserver diff --git a/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_2/partial.pbtxt b/qa/L0_perf_analyzer/nginx.conf similarity index 81% rename from qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_2/partial.pbtxt rename to qa/L0_perf_analyzer/nginx.conf index 11a75b4e9d..4a7dfcc04a 100644 --- a/qa/L0_model_config/custom_parameters/tensorrt/valid/allocation_strategy_value_2/partial.pbtxt +++ b/qa/L0_perf_analyzer/nginx.conf @@ -1,4 +1,4 @@ -# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -23,9 +23,16 @@ # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -parameters: { - key: "execution_context_allocation_strategy" - value: { - string_value: "ON_PROFILE_CHANGE" - } + +server { + listen 443 ssl; + server_name localhost; + + ssl_certificate /etc/nginx/cert.crt; + ssl_certificate_key /etc/nginx/cert.key; + + location / { + proxy_pass http://localhost:8000; + proxy_http_version 1.1; + } } diff --git a/qa/L0_perf_analyzer/perf_analyzer_profile_export_schema.json b/qa/L0_perf_analyzer/perf_analyzer_profile_export_schema.json new file mode 100644 index 0000000000..d0feacd9b4 --- /dev/null +++ b/qa/L0_perf_analyzer/perf_analyzer_profile_export_schema.json @@ -0,0 +1,95 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/triton-inference-server/client/blob/main/src/c%2B%2B/perf_analyzer/examples/schema.json", + "title": "Perf Analyzer output data", + "description": "A json file describing the output from a Perf Analyzer run.", + "type": "object", + "required": [ + "experiments", + "version" + ], + "properties": { + "experiments": { + "description": "The array of all experiments run by Perf Analyzer.", + "type": "array", + "required": [ + "experiment", + "requests", + "window_boundaries" + ], + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "object", + "properties": { + "experiment": { + "description": "A single experiment run by Perf Analyzer.", + "type": "object", + "required": [ + "mode", + "value" + ], + "minItems": 1, + "maxItems": 1, + "properties": { + "mode": { + "description": "Operating mode of Perf Analyzer: For example, 'concurrency' or 'request rate'.", + "type": "string" + }, + "value": { + "description": "Concurrency or request rate for the current experiment.", + "type": "integer" + } + } + }, + "requests": { + "description": "The array of requests sent by Perf Analyzer for this experiment.", + "type": "array", + "items": { + "$ref": "#/properties/experiments/items/properties/$defs/request" + } + }, + "$defs": { + "request": { + "description": "Info for a single request.", + "type": "object", + "required": [ + "timestamp", + "response_timestamps" + ], + "properties": { + "timestamp": { + "description": "Time stamp of the request.", + "type": "integer" + }, + "sequence_id": { + "description": "The sequence_id of the request.", + "type": "integer" + }, + "response_timestamps": { + "description": "All associated responses to this request.", + "type": "array", + "items": { + "type": "integer" + } + } + } + } + }, + "window_boundaries": { + "description": "An array of time stamps describing window boundaries.", + "type": "array", + "items": { + "type": "integer" + }, + "uniqueItems": true + } + } + } + }, + "version": { + "description": "The version of Perf Analyzer that generated the report.", + "type": "string" + } + } +} \ No newline at end of file diff --git a/qa/L0_perf_analyzer/test.sh b/qa/L0_perf_analyzer/test.sh new file mode 100755 index 0000000000..49c7e72e48 --- /dev/null +++ b/qa/L0_perf_analyzer/test.sh @@ -0,0 +1,1164 @@ +#!/bin/bash +# Copyright 2020-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +REPO_VERSION=${NVIDIA_TRITON_SERVER_VERSION} +if [ "$#" -ge 1 ]; then + REPO_VERSION=$1 +fi +if [ -z "$REPO_VERSION" ]; then + echo -e "Repository version must be specified" + echo -e "\n***\n*** Test Failed\n***" + exit 1 +fi +if [ ! -z "$TEST_REPO_ARCH" ]; then + REPO_VERSION=${REPO_VERSION}_${TEST_REPO_ARCH} +fi + +export CUDA_VISIBLE_DEVICES=0 + +CLIENT_LOG="./perf_analyzer.log" +PERF_ANALYZER=../clients/perf_analyzer + +DATADIR=`pwd`/models +TESTDATADIR=`pwd`/test_data + +INT_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/int_data.json +INT_DIFFSHAPE_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/int_data_diff_shape.json +INT_OPTIONAL_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/int_data_optional.json +FLOAT_DIFFSHAPE_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/float_data_with_shape.json +STRING_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/string_data.json +STRING_WITHSHAPE_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/string_data_with_shape.json +SEQ_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/seq_data.json +SHAPETENSORADTAFILE=`pwd`/../common/perf_analyzer_input_data_json/shape_tensor_data.json +IMAGE_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/image_data.json + +OUTPUT_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/output.json +NON_ALIGNED_OUTPUT_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/non_aligned_output.json +WRONG_OUTPUT_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/wrong_output.json +WRONG_OUTPUT_2_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/wrong_output_2.json + +SEQ_OUTPUT_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/seq_output.json +SEQ_WRONG_OUTPUT_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/seq_wrong_output.json + +REPEAT_INT32_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/repeat_int32_data.json + +TRACE_FILE="trace.json" + +SERVER=/opt/tritonserver/bin/tritonserver +SERVER_ARGS="--model-repository=${DATADIR} --trace-config triton,file=${TRACE_FILE}" +SERVER_LOG="./inference_server.log" + +ERROR_STRING="error | Request count: 0 | : 0 infer/sec" + +STABILITY_THRESHOLD="100" + +source ../common/util.sh + +rm -f $SERVER_LOG $CLIENT_LOG +rm -rf $DATADIR $TESTDATADIR $ENSEMBLE_DATADIR + +mkdir -p $DATADIR +# Copy fixed-shape models +cp -r /data/inferenceserver/${REPO_VERSION}/qa_model_repository/graphdef_int32_int32_int32 $DATADIR/ +cp -r /data/inferenceserver/${REPO_VERSION}/qa_model_repository/graphdef_nobatch_int32_int32_int32 $DATADIR/ +cp -r /data/inferenceserver/${REPO_VERSION}/qa_model_repository/graphdef_object_object_object $DATADIR/ +cp -r /data/inferenceserver/${REPO_VERSION}/qa_model_repository/graphdef_nobatch_object_object_object $DATADIR/ + +# Copy a variable-shape models +cp -r /data/inferenceserver/${REPO_VERSION}/qa_variable_model_repository/graphdef_object_int32_int32 $DATADIR/ +cp -r /data/inferenceserver/${REPO_VERSION}/qa_variable_model_repository/graphdef_int32_int32_float32 $DATADIR/ + +# Copy shape tensor models +cp -r /data/inferenceserver/${REPO_VERSION}/qa_shapetensor_model_repository/plan_zero_1_float32_int32 $DATADIR/ + +# Copying ensemble including a sequential model +cp -r /data/inferenceserver/${REPO_VERSION}/qa_sequence_model_repository/savedmodel_sequence_object $DATADIR +cp -r /data/inferenceserver/${REPO_VERSION}/qa_ensemble_model_repository/qa_sequence_model_repository/simple_savedmodel_sequence_object $DATADIR +cp -r /data/inferenceserver/${REPO_VERSION}/qa_ensemble_model_repository/qa_sequence_model_repository/nop_TYPE_FP32_-1 $DATADIR + +# Copying variable sequence model +cp -r /data/inferenceserver/${REPO_VERSION}/qa_variable_sequence_model_repository/graphdef_sequence_float32 $DATADIR + +mkdir $DATADIR/nop_TYPE_FP32_-1/1 + +# Copy inception model to the model repository +cp -r /data/inferenceserver/${REPO_VERSION}/tf_model_store/inception_v1_graphdef $DATADIR + +# Copy resnet50v1.5_fp16 +cp -r /data/inferenceserver/${REPO_VERSION}/perf_model_store/resnet50v1.5_fp16_savedmodel $DATADIR + +# Copy and customize custom_zero_1_float32 +cp -r ../custom_models/custom_zero_1_float32 $DATADIR && \ + mkdir $DATADIR/custom_zero_1_float32/1 && \ + (cd $DATADIR/custom_zero_1_float32 && \ + echo "parameters [" >> config.pbtxt && \ + echo "{ key: \"execute_delay_ms\"; value: { string_value: \"100\" }}" >> config.pbtxt && \ + echo "]" >> config.pbtxt) + +# Copy and customize optional inputs model +cp -r ../python_models/optional $DATADIR && \ + mkdir $DATADIR/optional/1 && \ + mv $DATADIR/optional/model.py $DATADIR/optional/1 && \ + sed -i 's/max_batch_size: 0/max_batch_size: 2/g' $DATADIR/optional/config.pbtxt + +# Copy decoupled model +git clone --depth=1 https://github.com/triton-inference-server/python_backend +mkdir -p $DATADIR/repeat_int32/1 +cp python_backend/examples/decoupled/repeat_config.pbtxt $DATADIR/repeat_int32/config.pbtxt +cp python_backend/examples/decoupled/repeat_model.py $DATADIR/repeat_int32/1/model.py + +# Generating test data +mkdir -p $TESTDATADIR +for INPUT in INPUT0 INPUT1; do + for i in {1..16}; do + echo '1' >> $TESTDATADIR/${INPUT} + done +done + +RET=0 + +run_server +if [ "$SERVER_PID" == "0" ]; then + echo -e "\n***\n*** Failed to start $SERVER\n***" + cat $SERVER_LOG + exit 1 +fi + + +# Test whether there was a conflict in sending sequences. This should +# be done before other testing as the server might emit this warning +# in certain test cases that are expected to raise this warning +SERVER_ERROR_STRING="The previous sequence did not end before this sequence start" + +set +e +$PERF_ANALYZER -v -i $PROTOCOL -m graphdef_object_object_object -p2000 -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 +if [ $? -eq 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed: Expected an error when using dynamic shapes in string inputs\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "input INPUT0 contains dynamic shape, provide shapes to send along with the request" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed: \n***" + RET=1 +fi + +# Testing with ensemble and sequential model variants +$PERF_ANALYZER -v -i grpc -m simple_savedmodel_sequence_object -p 2000 -t5 --streaming \ +--input-data=$SEQ_JSONDATAFILE --input-data=$SEQ_JSONDATAFILE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed: Sequence conflict when maintaining concurrency\n***" + RET=1 +fi + +$PERF_ANALYZER -v -i grpc -m simple_savedmodel_sequence_object -p 1000 --request-rate-range 100:200:50 --streaming \ +--input-data=$SEQ_JSONDATAFILE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi + +if [ $(cat $SERVER_LOG | grep "${SERVER_ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $SERVER_LOG | grep "${SERVER_ERROR_STRING}" + echo -e "\n***\n*** Test Failed: Sequence conflict\n***" + RET=1 +fi +set -e + +for PROTOCOL in grpc http; do + + # Testing simple configurations with different shared memory types + for SHARED_MEMORY_TYPE in none system cuda; do + set +e + $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_int32_int32_int32 -t 1 -p2000 -b 1 \ + --shared-memory=$SHARED_MEMORY_TYPE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + + $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_int32_int32_int32 -t 1 -p2000 -b 1 -a \ + --shared-memory=$SHARED_MEMORY_TYPE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + set -e + done + + # TODO Add back testing with preprocess_inception_ensemble model + + # Testing with inception model + for SHARED_MEMORY_TYPE in none system cuda; do + set +e + $PERF_ANALYZER -v -i $PROTOCOL -m inception_v1_graphdef -t 1 -p2000 -b 1 \ + --shared-memory=$SHARED_MEMORY_TYPE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + + $PERF_ANALYZER -v -i $PROTOCOL -m inception_v1_graphdef -t 1 -p2000 -b 1 -a \ + --shared-memory=$SHARED_MEMORY_TYPE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + set -e + done + + # Testing with resnet50 models with large batch sizes + for SHARED_MEMORY_TYPE in none system cuda; do + set +e + $PERF_ANALYZER -v -i $PROTOCOL -m inception_v1_graphdef -t 2 -p2000 -b 64 \ + --shared-memory=$SHARED_MEMORY_TYPE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + + $PERF_ANALYZER -v -i $PROTOCOL -m inception_v1_graphdef -t 2 -p2000 -b 64 \ + --shared-memory=$SHARED_MEMORY_TYPE -a -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + set -e + done + + # Test perf client behavior on different model with different batch size + for MODEL in graphdef_nobatch_int32_int32_int32 graphdef_int32_int32_int32; do + # Valid batch size + set +e + $PERF_ANALYZER -v -i $PROTOCOL -m $MODEL -t 1 -p2000 -b 1 -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + set -e + + # Invalid batch sizes + for STATIC_BATCH in 0 10; do + set +e + $PERF_ANALYZER -v -i $PROTOCOL -m $MODEL -t 1 -p2000 -b $STATIC_BATCH -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -eq 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + set -e + done + done + + # Testing with the new arguments + set +e + $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_int32_int32_int32 -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + + $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_int32_int32_int32 --concurrency-range 1:5:2 -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep "error | Request count: 0 | : 0 infer/sec\|: 0 usec|Request concurrency: 2" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + + $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_int32_int32_int32 --concurrency-range 1:5:2 \ + --input-data=${INT_JSONDATAFILE} -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep "error | Request count: 0 | : 0 infer/sec\|: 0 usec|Request concurrency: 2" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + + $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_int32_int32_int32 --request-rate-range 1000:2000:500 \ + -p1000 -b 1 -a -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + + $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_int32_int32_int32 --request-rate-range 1000:2000:500 \ + --input-data=${INT_JSONDATAFILE} -p1000 -b 1 -a -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + + # Binary search for request rate mode + $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_int32_int32_int32 --request-rate-range 1000:2000:100 -p1000 -b 1 \ + -a --binary-search --request-distribution "poisson" -l 10 -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + set -e + + # Binary search for concurrency range mode and make sure it doesn't hang + $PERF_ANALYZER -v -a --request-distribution "poisson" --shared-memory none \ + --percentile 99 --binary-search --concurrency-range 1:8:2 -l 5 \ + -m graphdef_int32_int32_int32 -b 1 -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 & + PA_PID=$! + if [ "$PA_PID" == "0" ]; then + echo -e "\n***\n*** Failed to start $PERF_ANALYZER\n***" + cat $CLIENT_LOG + RET=1 + fi + # wait for PA to finish running + sleep 200 + if ps -p $PA_PID > /dev/null; then + cat $CLIENT_LOG + echo -e "\n***\n*** $PERF_ANALYZER is hanging after 200 s\n***" + kill $PA_PID + RET=1 + fi + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + set -e + + # Testing with combinations of string input and shared memory types + for SHARED_MEMORY_TYPE in none system cuda; do + set +e + $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_object_object_object --string-data=1 -p2000 \ + --shared-memory=$SHARED_MEMORY_TYPE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + set -e + done + + # Testing with combinations of file inputs and shared memory types + for SHARED_MEMORY_TYPE in none system cuda; do + set +e + $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_object_object_object --input-data=$TESTDATADIR -p2000 \ + --shared-memory=$SHARED_MEMORY_TYPE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + set -e + done + + for SHARED_MEMORY_TYPE in none system cuda; do + set +e + $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_object_object_object --input-data=$STRING_JSONDATAFILE \ + --input-data=$STRING_JSONDATAFILE -p2000 --shared-memory=$SHARED_MEMORY_TYPE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + set -e + done + + # Testing with combinations of variable inputs and shared memory types + for SHARED_MEMORY_TYPE in none system cuda; do + set +e + $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_object_int32_int32 --input-data=$TESTDATADIR \ + --shape INPUT0:2,8 --shape INPUT1:2,8 -p2000 --shared-memory=$SHARED_MEMORY_TYPE -s ${STABILITY_THRESHOLD} \ + >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + set -e + done + + for SHARED_MEMORY_TYPE in none system cuda; do + set +e + $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_object_int32_int32 --input-data=$STRING_WITHSHAPE_JSONDATAFILE \ + --shape INPUT0:2,8 --shape INPUT1:2,8 -p2000 --shared-memory=$SHARED_MEMORY_TYPE -s ${STABILITY_THRESHOLD} \ + >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + set -e + done + + set +e + $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_int32_int32_float32 --shape INPUT0:2,8,2 \ + --shape INPUT1:2,8,2 -p2000 -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + set -e + + # Trying to batch tensors with different shape + for SHARED_MEMORY_TYPE in none system cuda; do + set +e + $PERF_ANALYZER -v -i $PROTOCOL -m graphdef_int32_int32_float32 --shape INPUT0:2,8,2 --shape INPUT1:2,8,2 -p2000 -b 4 \ + --shared-memory=$SHARED_MEMORY_TYPE --input-data=$INT_DIFFSHAPE_JSONDATAFILE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -eq 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep -P "The supplied shape .+ is incompatible with the model's input shape" | wc -l) -eq 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + set -e + done + + # Shape tensor I/O model (server needs the shape tensor on the CPU) + for SHARED_MEMORY_TYPE in none system; do + set +e + $PERF_ANALYZER -v -i $PROTOCOL -m plan_zero_1_float32_int32 --input-data=$SHAPETENSORADTAFILE \ + --shape DUMMY_INPUT0:4,4 -p2000 --shared-memory=$SHARED_MEMORY_TYPE -b 8 -s ${STABILITY_THRESHOLD} \ + >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep ": 0 infer/sec\|: 0 usec" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + set -e + done + + set +e + $PERF_ANALYZER -v -i $PROTOCOL -m simple_savedmodel_sequence_object -p 2000 -t5 --sync \ + --input-data=$SEQ_JSONDATAFILE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + + $PERF_ANALYZER -v -i $PROTOCOL -m simple_savedmodel_sequence_object -p 2000 -t5 --sync \ + --input-data=$SEQ_JSONDATAFILE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + + $PERF_ANALYZER -v -i $PROTOCOL -m simple_savedmodel_sequence_object -p 1000 --request-rate-range 100:200:50 --sync \ + --input-data=$SEQ_JSONDATAFILE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + set -e + + + # Testing with variable ensemble model. This unit specifies different shape values + # for different inferences. + for SHARED_MEMORY_TYPE in none system cuda; do + set +e + # FIXME: Enable HTTP when the server is able to correctly return the complex error messages. + $PERF_ANALYZER -v -i grpc -m graphdef_sequence_float32 --shape INPUT:2 --input-data=$FLOAT_DIFFSHAPE_JSONDATAFILE \ + --input-data=$FLOAT_DIFFSHAPE_JSONDATAFILE -p2000 --shared-memory=$SHARED_MEMORY_TYPE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -eq 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep -P "The supplied shape .+ is incompatible with the model's input shape" | wc -l) -eq 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + set -e + done + + # Testing that trace logging works + set +e + rm ${TRACE_FILE}* + $PERF_ANALYZER -v -i $PROTOCOL -m simple_savedmodel_sequence_object -p 2000 -t5 --sync \ + --trace-level TIMESTAMPS --trace-rate 1000 --trace-count 100 --log-frequency 10 \ + --input-data=$SEQ_JSONDATAFILE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if ! compgen -G "$TRACE_FILE*" > /dev/null; then + echo -e "\n***\n*** Test Failed. $TRACE_FILE failed to generate.\n***" + RET=1 + elif [ $(cat ${TRACE_FILE}* | grep "REQUEST_START" | wc -l) -eq 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed. Did not find `REQUEST_START` in $TRACE_FILE \n***" + RET=1 + fi + curl localhost:8000/v2/trace/setting -d '{"trace_level":["OFF"]}' + set -e + + # Testing that setting trace file does not work + set +e + $PERF_ANALYZER -v -i $PROTOCOL -m simple_savedmodel_sequence_object \ + --trace-file $TRACE_FILE >$CLIENT_LOG 2>&1 + if [ $? -eq 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed. Expected to fail for unknown arg --trace-file" + RET=1 + fi + curl localhost:8000/v2/trace/setting -d '{"trace_level":["OFF"]}' + set -e +done + +# Test with output validation +set +e +$PERF_ANALYZER -v -m graphdef_int32_int32_int32 --input-data=${NON_ALIGNED_OUTPUT_JSONDATAFILE} -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 +if [ $? -eq 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "The 'validation_data' field doesn't align with 'data' field in the json file" | wc -l) -eq 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi + +$PERF_ANALYZER -v -m graphdef_int32_int32_int32 --input-data=${WRONG_OUTPUT_JSONDATAFILE} -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 +if [ $? -eq 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "mismatch in the data provided" | wc -l) -eq 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi + +$PERF_ANALYZER -v -m graphdef_int32_int32_int32 --input-data=${WRONG_OUTPUT_2_JSONDATAFILE} -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 +if [ $? -eq 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "Output doesn't match expected output" | wc -l) -eq 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi + + +$PERF_ANALYZER -v -m graphdef_int32_int32_int32 --input-data=${OUTPUT_JSONDATAFILE} -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi + +$PERF_ANALYZER -v -m simple_savedmodel_sequence_object -i grpc --streaming \ +--input-data=${SEQ_WRONG_OUTPUT_JSONDATAFILE} -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 +if [ $? -eq 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "Output doesn't match expected output" | wc -l) -eq 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi + +$PERF_ANALYZER -v -m simple_savedmodel_sequence_object -i grpc --streaming \ +--input-data=${SEQ_OUTPUT_JSONDATAFILE} -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +set -e + +## Testing with very large concurrencies and large dataset +INPUT_DATA_OPTION="--input-data $SEQ_JSONDATAFILE " +for i in {1..9}; do + INPUT_DATA_OPTION=" ${INPUT_DATA_OPTION} ${INPUT_DATA_OPTION}" +done +set +e +$PERF_ANALYZER -v -m simple_savedmodel_sequence_object -p 10000 --concurrency-range 1500:2000:250 -i grpc --streaming \ +${INPUT_DATA_OPTION} -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +set -e + +## Test count_windows mode +set +e + +# Send incorrect shape and make sure that perf_analyzer doesn't hang +$PERF_ANALYZER -v -m graphdef_object_int32_int32 --measurement-mode "count_windows" \ + --shape INPUT0:1,8,100 --shape INPUT1:2,8 --string-data=1 -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 +if [ $? -eq 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "unexpected shape for input 'INPUT0' for model" | wc -l) -eq 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi + +$PERF_ANALYZER -v -m graphdef_object_int32_int32 --measurement-mode "count_windows" \ + --shape INPUT0:2,8 --shape INPUT1:2,8 --string-data=1 -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +set -e + +# Test with optional inputs missing but still valid +set +e +$PERF_ANALYZER -v -m optional --measurement-mode "count_windows" \ + --input-data=${INT_OPTIONAL_JSONDATAFILE} -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +set -e + +# Test with optional inputs missing and invalid +set +e +OPTIONAL_INPUT_ERROR_STRING="For batch sizes larger than 1, the same set of +inputs must be specified for each batch. You cannot use different set of +optional inputs for each individual batch." +$PERF_ANALYZER -v -m optional -b 2 --measurement-mode "count_windows" \ + --input-data=${INT_OPTIONAL_JSONDATAFILE} -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 +if [ $? -eq 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "${OPTIONAL_INPUT_ERROR_STRING}" | wc -l) -eq 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +set -e + + +# Test Custom request rate option +CUSTOM_SCHEDULE_FILE=$TESTDATADIR/custom.schedule +echo '30000' >> $CUSTOM_SCHEDULE_FILE +echo '10000' >> $CUSTOM_SCHEDULE_FILE +echo '40000' >> $CUSTOM_SCHEDULE_FILE +echo '20000' >> $CUSTOM_SCHEDULE_FILE +echo '25000' >> $CUSTOM_SCHEDULE_FILE + +set +e +$PERF_ANALYZER -v -i grpc -m graphdef_int32_int32_int32 --request-intervals $CUSTOM_SCHEDULE_FILE >$CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "Request Rate: 40" | wc -l) -eq 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed: \n***" + RET=1 +fi +set -e + +# Test --serial-sequences mode +set +e +$PERF_ANALYZER -v -i $PROTOCOL -m simple_savedmodel_sequence_object -p 1000 --request-rate-range 100:200:50 --serial-sequences \ + --input-data=$SEQ_JSONDATAFILE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi + +$PERF_ANALYZER -v -i $PROTOCOL -m simple_savedmodel_sequence_object -p 1000 --request-intervals $CUSTOM_SCHEDULE_FILE --serial-sequences \ + --input-data=$SEQ_JSONDATAFILE -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +set -e + +## Test decoupled model support +$PERF_ANALYZER -v -m repeat_int32 --input-data=$REPEAT_INT32_JSONDATAFILE \ + --profile-export-file profile_export.json -i grpc --async --streaming -s \ + ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +python3 -c "import json ; \ + requests = json.load(open('profile_export.json'))['experiments'][0]['requests'] ; \ + assert any(len(r['response_timestamps']) > 1 for r in requests)" +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +check-jsonschema --schemafile perf_analyzer_profile_export_schema.json profile_export.json +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi + +## Test perf_analyzer with MPI / multiple models + +is_synchronized() { + local TIMESTAMP_RANK_0_STABLE=$(grep -oP "^\K[^$]+(?=\[1,0\]:All models on all MPI ranks are stable)" 1/rank.0/stdout | date "+%s" -f -) + local TIMESTAMP_RANK_1_STABLE=$(grep -oP "^\K[^$]+(?=\[1,1\]:All models on all MPI ranks are stable)" 1/rank.1/stdout | date "+%s" -f -) + local TIMESTAMP_RANK_2_STABLE=$(grep -oP "^\K[^$]+(?=\[1,2\]:All models on all MPI ranks are stable)" 1/rank.2/stdout | date "+%s" -f -) + local TIMESTAMP_MIN=$(echo -e "${TIMESTAMP_RANK_0_STABLE}\n${TIMESTAMP_RANK_1_STABLE}\n${TIMESTAMP_RANK_2_STABLE}" | sort -n | head -1) + local TIMESTAMP_MAX=$(echo -e "${TIMESTAMP_RANK_0_STABLE}\n${TIMESTAMP_RANK_1_STABLE}\n${TIMESTAMP_RANK_2_STABLE}" | sort -n | tail -1) + local TIMESTAMP_MAX_MIN_DIFFERENCE=$((${TIMESTAMP_MAX}-${TIMESTAMP_MIN})) + local ALLOWABLE_SECONDS_BETWEEN_PROFILES_FINISHING="5" + echo $(($TIMESTAMP_MAX_MIN_DIFFERENCE <= $ALLOWABLE_SECONDS_BETWEEN_PROFILES_FINISHING)) +} + +is_stable() { + local RANK=$1 + local IS_THROUGHPUT=$2 + if [ $IS_THROUGHPUT ]; then + local GREP_PATTERN="\[1,$RANK\]: Pass \[[0-9]+\] throughput: \K[0-9]+\.?[0-9]*" + else + local GREP_PATTERN="\[1,$RANK\]: Pass \[[0-9]+\] throughput: [0-9]+\.?[0-9]* infer/sec. Avg latency: \K[0-9]+" + fi + local LAST_MINUS_0=$(grep -oP "$GREP_PATTERN" 1/rank.$RANK/stdout | tail -3 | sed -n 3p) + local LAST_MINUS_1=$(grep -oP "$GREP_PATTERN" 1/rank.$RANK/stdout | tail -3 | sed -n 2p) + local LAST_MINUS_2=$(grep -oP "$GREP_PATTERN" 1/rank.$RANK/stdout | tail -3 | sed -n 1p) + local MEAN=$(awk "BEGIN {print (($LAST_MINUS_0+$LAST_MINUS_1+$LAST_MINUS_2)/3)}") + local STABILITY_THRESHOLD=0.5 + # Based on this: https://github.com/triton-inference-server/client/blob/main/src/c++/perf_analyzer/inference_profiler.cc#L629-L644 + local WITHIN_THRESHOLD_0=$(awk "BEGIN {print ($LAST_MINUS_0 >= ((1 - $STABILITY_THRESHOLD) * $MEAN) && $LAST_MINUS_0 <= ((1 + $STABILITY_THRESHOLD) * $MEAN))}") + local WITHIN_THRESHOLD_1=$(awk "BEGIN {print ($LAST_MINUS_1 >= ((1 - $STABILITY_THRESHOLD) * $MEAN) && $LAST_MINUS_1 <= ((1 + $STABILITY_THRESHOLD) * $MEAN))}") + local WITHIN_THRESHOLD_2=$(awk "BEGIN {print ($LAST_MINUS_2 >= ((1 - $STABILITY_THRESHOLD) * $MEAN) && $LAST_MINUS_2 <= ((1 + $STABILITY_THRESHOLD) * $MEAN))}") + echo $(($WITHIN_THRESHOLD_0 && $WITHIN_THRESHOLD_1 && $WITHIN_THRESHOLD_2)) +} + +set +e +mpiexec --allow-run-as-root \ + -n 1 --merge-stderr-to-stdout --output-filename . --tag-output --timestamp-output \ + $PERF_ANALYZER -v -m graphdef_int32_int32_int32 \ + --measurement-mode count_windows -s 50 --enable-mpi : \ + -n 1 --merge-stderr-to-stdout --output-filename . --tag-output --timestamp-output \ + $PERF_ANALYZER -v -m graphdef_nobatch_int32_int32_int32 \ + --measurement-mode count_windows -s 50 --enable-mpi : \ + -n 1 --merge-stderr-to-stdout --output-filename . --tag-output --timestamp-output \ + $PERF_ANALYZER -v -m custom_zero_1_float32 \ + --measurement-mode count_windows -s 50 --enable-mpi +if [ $? -ne 0 ]; then + cat 1/rank.0/stdout 1/rank.2/stdout 1/rank.2/stdout + echo -e "\n***\n*** Perf Analyzer returned non-zero exit code\n***" + echo -e "\n***\n*** Test Failed\n***" + RET=1 +else + if [ $(is_synchronized) -eq 0 ]; then + cat 1/rank.0/stdout 1/rank.2/stdout 1/rank.2/stdout + echo -e "\n***\n*** All models did not finish profiling at almost the same time\n***" + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + + RANK_0_THROUGHPUT_IS_STABLE=$(is_stable 0 1) + RANK_0_LATENCY_IS_STABLE=$(is_stable 0 0) + RANK_1_THROUGHPUT_IS_STABLE=$(is_stable 1 1) + RANK_1_LATENCY_IS_STABLE=$(is_stable 1 0) + RANK_2_THROUGHPUT_IS_STABLE=$(is_stable 2 1) + RANK_2_LATENCY_IS_STABLE=$(is_stable 2 0) + + ALL_STABLE=$(( \ + $RANK_0_THROUGHPUT_IS_STABLE && \ + $RANK_0_LATENCY_IS_STABLE && \ + $RANK_1_THROUGHPUT_IS_STABLE && \ + $RANK_1_LATENCY_IS_STABLE && \ + $RANK_2_THROUGHPUT_IS_STABLE && \ + $RANK_2_LATENCY_IS_STABLE)) + + if [ $ALL_STABLE -eq 0 ]; then + cat 1/rank.0/stdout 1/rank.2/stdout 1/rank.2/stdout + echo -e "\n***\n*** All models did not stabilize\n***" + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + + rm -rf 1 +fi +set -e + +## Test perf_analyzer without MPI library (`libmpi.so`) available + +rm -rf /opt/hpcx/ompi/lib/libmpi* + +set +e +$PERF_ANALYZER -v -m graphdef_int32_int32_int32 -s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +set -e + +kill $SERVER_PID +wait $SERVER_PID + +# Generate valid CA +openssl genrsa -passout pass:1234 -des3 -out ca.key 4096 +openssl req -passin pass:1234 -new -x509 -days 365 -key ca.key -out ca.crt -subj "/C=SP/ST=Spain/L=Valdepenias/O=Test/OU=Test/CN=Root CA" + +# Generate valid Server Key/Cert +openssl genrsa -passout pass:1234 -des3 -out server.key 4096 +openssl req -passin pass:1234 -new -key server.key -out server.csr -subj "/C=SP/ST=Spain/L=Valdepenias/O=Test/OU=Server/CN=localhost" +openssl x509 -req -passin pass:1234 -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt + +# Remove passphrase from the Server Key +openssl rsa -passin pass:1234 -in server.key -out server.key + +# Generate valid Client Key/Cert +openssl genrsa -passout pass:1234 -des3 -out client.key 4096 +openssl req -passin pass:1234 -new -key client.key -out client.csr -subj "/C=SP/ST=Spain/L=Valdepenias/O=Test/OU=Client/CN=localhost" +openssl x509 -passin pass:1234 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt + +# Remove passphrase from Client Key +openssl rsa -passin pass:1234 -in client.key -out client.key + +# Create mutated client key (Make first char of each like capital) +cp client.key client2.key && sed -i "s/\b\(.\)/\u\1/g" client2.key +cp client.crt client2.crt && sed -i "s/\b\(.\)/\u\1/g" client2.crt + +SERVER_ARGS="--model-repository=${DATADIR} --grpc-use-ssl=1 --grpc-server-cert=server.crt --grpc-server-key=server.key --grpc-root-cert=ca.crt" + +run_server +if [ "$SERVER_PID" == "0" ]; then + echo -e "\n***\n*** Failed to start $SERVER\n***" + cat $SERVER_LOG + exit 1 +fi + +# Test gRPC SSL +set +e + +# Test that gRPC protocol with SSL works correctly +$PERF_ANALYZER -v -i grpc -m graphdef_int32_int32_int32 \ + --ssl-grpc-use-ssl \ + --ssl-grpc-root-certifications-file=ca.crt \ + --ssl-grpc-private-key-file=client.key \ + --ssl-grpc-certificate-chain-file=client.crt \ + -s ${STABILITY_THRESHOLD} \ + > ${CLIENT_LOG}.grpc_success 2>&1 +if [ $? -ne 0 ]; then + cat ${CLIENT_LOG}.grpc_success + RET=1 +fi + +# Test that gRPC protocol with SSL fails with incorrect key +$PERF_ANALYZER -v -i grpc -m graphdef_int32_int32_int32 \ + --ssl-grpc-use-ssl \ + --ssl-grpc-root-certifications-file=ca.crt \ + --ssl-grpc-private-key-file=client.key \ + --ssl-grpc-certificate-chain-file=client2.crt \ + -s ${STABILITY_THRESHOLD} \ + > ${CLIENT_LOG}.grpc_failure 2>&1 +if [ $? -eq 0 ]; then + cat ${CLIENT_LOG}.grpc_failure + echo -e "\n***\n*** Expected test failure\n***" + RET=1 +fi + +set -e + +kill $SERVER_PID +wait $SERVER_PID + +cp server.crt /etc/nginx/cert.crt +cp server.key /etc/nginx/cert.key + +SERVER_ARGS="--model-repository=${DATADIR}" + +run_server +if [ "$SERVER_PID" == "0" ]; then + echo -e "\n***\n*** Failed to start $SERVER\n***" + cat $SERVER_LOG + exit 1 +fi + +# Setup the new configuration for the proxy. The HTTPS traffic will be +# redirected to the running instance of server at localhost:8000 +cp nginx.conf /etc/nginx/sites-available/default + +# Start the proxy server +service nginx restart + +# Test HTTP SSL +set +e + +# Test that HTTP protocol with SSL works correctly with certificates +$PERF_ANALYZER -v -u https://localhost:443 -i http -m graphdef_int32_int32_int32 \ + --ssl-https-verify-peer 1 \ + --ssl-https-verify-host 2 \ + --ssl-https-ca-certificates-file ca.crt \ + --ssl-https-client-certificate-file client.crt \ + --ssl-https-client-certificate-type PEM \ + --ssl-https-private-key-file client.key \ + --ssl-https-private-key-type PEM \ + -s ${STABILITY_THRESHOLD} \ + > ${CLIENT_LOG}.https_success 2>&1 +if [ $? -ne 0 ]; then + cat ${CLIENT_LOG}.https_success + RET=1 +fi + +# Test that HTTP protocol with SSL works correctly without certificates +$PERF_ANALYZER -v -u https://localhost:443 -i http -m graphdef_int32_int32_int32 \ + --ssl-https-verify-peer 0 \ + --ssl-https-verify-host 0 \ + -s ${STABILITY_THRESHOLD} \ + > ${CLIENT_LOG}.https_success 2>&1 +if [ $? -ne 0 ]; then + cat ${CLIENT_LOG}.https_success + RET=1 +fi + +# Test that HTTP protocol with SSL fails with incorrect key +$PERF_ANALYZER -v -u https://localhost:443 -i http -m graphdef_int32_int32_int32 \ + --ssl-https-verify-peer 1 \ + --ssl-https-verify-host 2 \ + --ssl-https-ca-certificates-file ca.crt \ + --ssl-https-client-certificate-file client.crt \ + --ssl-https-client-certificate-type PEM \ + --ssl-https-private-key-file client2.key \ + --ssl-https-private-key-type PEM \ + -s ${STABILITY_THRESHOLD} \ + > ${CLIENT_LOG}.https_failure 2>&1 +if [ $? -eq 0 ]; then + cat ${CLIENT_LOG}.https_failure + echo -e "\n***\n*** Expected test failure\n***" + RET=1 +fi + +set -e + +kill $SERVER_PID +wait $SERVER_PID + +if [ $RET -eq 0 ]; then + echo -e "\n***\n*** Test Passed\n***" +else + echo -e "\n***\n*** Test FAILED\n***" +fi + +exit $RET diff --git a/qa/L0_perf_analyzer_capi/test.sh b/qa/L0_perf_analyzer_capi/test.sh new file mode 100755 index 0000000000..53196fa762 --- /dev/null +++ b/qa/L0_perf_analyzer_capi/test.sh @@ -0,0 +1,320 @@ +#!/bin/bash +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# TESTS COPIED FROM L0_perf_analyzer/test.sh +REPO_VERSION=${NVIDIA_TRITON_SERVER_VERSION} +if [ "$#" -ge 1 ]; then + REPO_VERSION=$1 +fi +if [ -z "$REPO_VERSION" ]; then + echo -e "Repository version must be specified" + echo -e "\n***\n*** Test Failed\n***" + exit 1 +fi +if [ ! -z "$TEST_REPO_ARCH" ]; then + REPO_VERSION=${REPO_VERSION}_${TEST_REPO_ARCH} +fi + +export CUDA_VISIBLE_DEVICES=0 + +CLIENT_LOG="./perf_analyzer.log" +PERF_ANALYZER=../clients/perf_analyzer + +DATADIR=`pwd`/models +TESTDATADIR=`pwd`/test_data + +SERVER_LIBRARY_PATH=/opt/tritonserver + +FLOAT_DIFFSHAPE_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/float_data_with_shape.json +STRING_WITHSHAPE_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/string_data_with_shape.json +SEQ_JSONDATAFILE=`pwd`/../common/perf_analyzer_input_data_json/seq_data.json +SHAPETENSORADTAFILE=`pwd`/../common/perf_analyzer_input_data_json/shape_tensor_data.json + +ERROR_STRING="error | Request count: 0 | : 0 infer/sec" + +STABILITY_THRESHOLD="9999" + +source ../common/util.sh + +rm -f $CLIENT_LOG +rm -rf $DATADIR $TESTDATADIR $ENSEMBLE_DATADIR + +mkdir -p $DATADIR +# Copy fixed-shape models +cp -r /data/inferenceserver/${REPO_VERSION}/qa_model_repository/graphdef_int32_int32_int32 $DATADIR/ +cp -r /data/inferenceserver/${REPO_VERSION}/qa_model_repository/graphdef_object_object_object $DATADIR/ + +# Copy a variable-shape models +cp -r /data/inferenceserver/${REPO_VERSION}/qa_variable_model_repository/graphdef_object_int32_int32 $DATADIR/ +cp -r /data/inferenceserver/${REPO_VERSION}/qa_variable_model_repository/graphdef_int32_int32_float32 $DATADIR/ + +# Copy shape tensor models +cp -r /data/inferenceserver/${REPO_VERSION}/qa_shapetensor_model_repository/plan_zero_1_float32_int32 $DATADIR/ + +# Copying ensemble including a sequential model +cp -r /data/inferenceserver/${REPO_VERSION}/qa_sequence_model_repository/savedmodel_sequence_object $DATADIR +cp -r /data/inferenceserver/${REPO_VERSION}/qa_ensemble_model_repository/qa_sequence_model_repository/simple_savedmodel_sequence_object $DATADIR + +# Copying variable sequence model +cp -r /data/inferenceserver/${REPO_VERSION}/qa_variable_sequence_model_repository/graphdef_sequence_float32 $DATADIR + +# Copying bls model with undefined variable +mkdir -p $DATADIR/bls_undefined/1 && \ + cp ../python_models/bls_undefined/model.py $DATADIR/bls_undefined/1/. && \ + cp ../python_models/bls_undefined/config.pbtxt $DATADIR/bls_undefined/. + +# Generating test data +mkdir -p $TESTDATADIR +for INPUT in INPUT0 INPUT1; do + for i in {1..16}; do + echo '1' >> $TESTDATADIR/${INPUT} + done +done + +RET=0 + +########## Test C API ############# +# Make sure tritonserver is not running first +set +e +SERVER_PID=$(pidof tritonserver) +if [ $? -ne 1 ]; then +echo -e "\n There was a previous instance of tritonserver, killing \n" + kill $SERVER_PID + wait $SERVER_PID +fi +set -e + +# Testing simple configuration +$PERF_ANALYZER -v -m graphdef_int32_int32_int32 \ +--service-kind=triton_c_api \ +--model-repository=$DATADIR --triton-server-directory=$SERVER_LIBRARY_PATH \ +-s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi + +$PERF_ANALYZER -v -m graphdef_int32_int32_int32 -t 1 -p2000 -b 1 \ +--service-kind=triton_c_api --model-repository=$DATADIR \ +--triton-server-directory=$SERVER_LIBRARY_PATH -s ${STABILITY_THRESHOLD} \ +>$CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi + +#Testing with string input +$PERF_ANALYZER -v -m graphdef_object_object_object --string-data=1 -p2000 \ +--service-kind=triton_c_api --model-repository=$DATADIR \ +--triton-server-directory=$SERVER_LIBRARY_PATH -s ${STABILITY_THRESHOLD} \ +>$CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi + +# Testing with variable inputs +$PERF_ANALYZER -v -m graphdef_object_int32_int32 --input-data=$TESTDATADIR \ +--shape INPUT0:2,8 --shape INPUT1:2,8 \ +--service-kind=triton_c_api --model-repository=$DATADIR \ +--triton-server-directory=$SERVER_LIBRARY_PATH -s ${STABILITY_THRESHOLD} \ +>$CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi + +$PERF_ANALYZER -v -m graphdef_object_int32_int32 \ +--input-data=$STRING_WITHSHAPE_JSONDATAFILE \ +--shape INPUT0:2,8 --shape INPUT1:2,8 -p2000 \ +--service-kind=triton_c_api --model-repository=$DATADIR \ +--triton-server-directory=$SERVER_LIBRARY_PATH -s ${STABILITY_THRESHOLD} \ +>$CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi + +$PERF_ANALYZER -v -m graphdef_int32_int32_float32 --shape INPUT0:2,8,2 \ +--shape INPUT1:2,8,2 -p2000 \ +--service-kind=triton_c_api --model-repository=$DATADIR \ +--triton-server-directory=$SERVER_LIBRARY_PATH -s ${STABILITY_THRESHOLD} \ +>$CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi + +# Shape tensor I/O model (server needs the shape tensor on the CPU) +$PERF_ANALYZER -v -m plan_zero_1_float32_int32 --input-data=$SHAPETENSORADTAFILE \ +--shape DUMMY_INPUT0:4,4 -p2000 -b 8 \ +--service-kind=triton_c_api --model-repository=$DATADIR \ +--triton-server-directory=$SERVER_LIBRARY_PATH -s ${STABILITY_THRESHOLD} \ +>$CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep ": 0 infer/sec\|: 0 usec" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi + +$PERF_ANALYZER -v -m simple_savedmodel_sequence_object -p 2000 -t5 --sync \ +-s ${STABILITY_THRESHOLD} \ +--input-data=$SEQ_JSONDATAFILE \ +--service-kind=triton_c_api --model-repository=$DATADIR \ +--triton-server-directory=$SERVER_LIBRARY_PATH >$CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi + +set +e +$PERF_ANALYZER -v -m graphdef_sequence_float32 --shape INPUT:2 \ +-s ${STABILITY_THRESHOLD} \ +--input-data=$FLOAT_DIFFSHAPE_JSONDATAFILE \ +--input-data=$FLOAT_DIFFSHAPE_JSONDATAFILE -p2000 \ +--service-kind=triton_c_api --model-repository=$DATADIR \ +--triton-server-directory=$SERVER_LIBRARY_PATH --sync >$CLIENT_LOG 2>&1 +if [ $? -eq 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep -P "The supplied shape .+ is incompatible with the model's input shape" | wc -l) -eq 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +set -e + +for SHARED_MEMORY_TYPE in system cuda; do + $PERF_ANALYZER -v -m graphdef_int32_int32_int32 -t 1 -p2000 -b 1 \ + -s ${STABILITY_THRESHOLD} \ + --shared-memory=$SHARED_MEMORY_TYPE \ + --service-kind=triton_c_api --model-repository=$DATADIR \ + --triton-server-directory=$SERVER_LIBRARY_PATH >$CLIENT_LOG 2>&1 + if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi +done + + +$PERF_ANALYZER -v -m graphdef_int32_int32_int32 --request-rate-range 1000:2000:500 -p1000 -b 1 \ +--service-kind=triton_c_api --model-repository=$DATADIR \ +--triton-server-directory=$SERVER_LIBRARY_PATH -s ${STABILITY_THRESHOLD} \ +>$CLIENT_LOG 2>&1 +if [ $? -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +if [ $(cat $CLIENT_LOG | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +set -e + +set +e +# Testing erroneous configuration +# This model is expected to fail +$PERF_ANALYZER -v -m bls_undefined --shape INPUT0:1048576 -t 64\ +--service-kind=triton_c_api \ +--model-repository=$DATADIR --triton-server-directory=$SERVER_LIBRARY_PATH \ +-s ${STABILITY_THRESHOLD} >$CLIENT_LOG 2>&1 +if [ $? -ne 99 ]; then + cat $CLIENT_LOG + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +set -e + +# Make sure server is not still running +set +e +SERVER_PID=$(pidof tritonserver) +if [ $? -eq 0 ]; then + echo -e "\n Tritonserver did not exit properly, killing \n" + kill $SERVER_PID + wait $SERVER_PID + RET=1 +fi +set -e + +if [ $RET -eq 0 ]; then + echo -e "\n***\n*** Test Passed\n***" +else + echo -e "\n***\n*** Test FAILED\n***" +fi +exit $RET diff --git a/python/openai/tests/vllm_embedding_models/all-MiniLM-L6-v2/config.pbtxt b/qa/L0_perf_analyzer_doc_links/mkdocs.yml similarity index 85% rename from python/openai/tests/vllm_embedding_models/all-MiniLM-L6-v2/config.pbtxt rename to qa/L0_perf_analyzer_doc_links/mkdocs.yml index 39b3c48edb..41a4bfe485 100644 --- a/python/openai/tests/vllm_embedding_models/all-MiniLM-L6-v2/config.pbtxt +++ b/qa/L0_perf_analyzer_doc_links/mkdocs.yml @@ -1,4 +1,4 @@ -# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -24,5 +24,13 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -backend: "vllm" -instance_group [{kind: KIND_MODEL}] +site_name: CI Test +use_directory_urls: False +docs_dir: "./docs" +plugins: + - htmlproofer + - search + +markdown_extensions: + - toc: + permalink: True diff --git a/qa/L0_backend_onnxruntime/test.sh b/qa/L0_perf_analyzer_doc_links/test.sh similarity index 55% rename from qa/L0_backend_onnxruntime/test.sh rename to qa/L0_perf_analyzer_doc_links/test.sh index 2e66b467e9..d0757bca9e 100755 --- a/qa/L0_backend_onnxruntime/test.sh +++ b/qa/L0_perf_analyzer_doc_links/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -25,67 +25,50 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -REPO_VERSION=${NVIDIA_TRITON_SERVER_VERSION} -if [ "$#" -ge 1 ]; then - REPO_VERSION=$1 -fi -if [ -z "$REPO_VERSION" ]; then - echo -e "Repository version must be specified" - echo -e "\n***\n*** Test Failed\n***" - exit 1 -fi -if [ ! -z "$TEST_REPO_ARCH" ]; then - REPO_VERSION=${REPO_VERSION}_${TEST_REPO_ARCH} -fi - -export CUDA_VISIBLE_DEVICES=0 - -DATADIR=/data/inferenceserver/${REPO_VERSION} -SERVER=/opt/tritonserver/bin/tritonserver -SERVER_LOG="./inference_server.log" -CLIENT_LOG="./test.log" -source ../common/util.sh - -rm -f *.log -rm -rf models - +LOG="`pwd`/doc_links.log" +CONFIG="`pwd`/mkdocs.yml" RET=0 -# BFLOAT16 test -mkdir -p models -cp -r ${DATADIR}/qa_model_repository/onnx_bf16_bf16_bf16 models/. +# Download necessary packages +python3 -m pip install mkdocs +python3 -m pip install mkdocs-htmlproofer-plugin==0.10.3 -SERVER_ARGS="--model-repository=`pwd`/models" -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 -fi +#Download perf_analyzer docs +TRITON_REPO_ORGANIZATION=${TRITON_REPO_ORGANIZATION:="http://github.com/triton-inference-server"} +TRITON_PERF_ANALYZER_REPO_TAG="${TRITON_PERF_ANALYZER_REPO_TAG:=main}" +git clone -b ${TRITON_PERF_ANALYZER_REPO_TAG} ${TRITON_REPO_ORGANIZATION}/perf_analyzer.git +cp `pwd`/perf_analyzer/README.md . +cp -rf `pwd`/perf_analyzer/docs . + +# Need to remove all links that start with -- or -. Mkdocs converts all -- to - for anchor links. +# This breaks all links to cli commands throughout the docs. This will iterate over all +# files in the docs directory and remove -- and - at the start of options, which allows the +# tool to check links for correctness. +for file in `pwd`/docs/*.md +do + echo $file + sed -i 's/`-*/`/g' $file + sed -i 's/#-*/#/g' $file +done -set +e +exec mkdocs serve -f $CONFIG > $LOG & +PID=$! +sleep 20 -for client_type in http grpc; do - export CLIENT_TYPE=$client_type - CLIENT_LOG="./test_${client_type}.log" - python test.py >>$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed ($client_type)\n***" - RET=1 - fi +until [[ (-z `pgrep mkdocs`) ]]; do + kill -2 $PID + sleep 2 done -unset CLIENT_TYPE -set -e +if [[ ! -z `grep "invalid url" $LOG` ]]; then + cat $LOG + RET=1 +fi -kill $SERVER_PID -wait $SERVER_PID if [ $RET -eq 0 ]; then - echo -e "\n***\n*** Test Passed\n***" + echo -e "\n***\n*** Test PASSED\n***" else echo -e "\n***\n*** Test FAILED\n***" fi - exit $RET diff --git a/qa/L0_perf_analyzer_ground_truth/test.sh b/qa/L0_perf_analyzer_ground_truth/test.sh new file mode 100755 index 0000000000..d5d78e63f4 --- /dev/null +++ b/qa/L0_perf_analyzer_ground_truth/test.sh @@ -0,0 +1,175 @@ +#!/bin/bash +# Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +REPO_VERSION=${NVIDIA_TRITON_SERVER_VERSION} +if [ "$#" -ge 1 ]; then + REPO_VERSION=$1 +fi +if [ -z "${REPO_VERSION}" ]; then + echo -e "Repository version must be specified" + echo -e "\n***\n*** Test Failed\n***" + exit 1 +fi +if [ ! -z "$TEST_REPO_ARCH" ]; then + REPO_VERSION=${REPO_VERSION}_${TEST_REPO_ARCH} +fi + +source ../common/util.sh + +# Setup client/perf_analyzer +CLIENT_LOG="./perf_analyzer.log" +PERF_ANALYZER=../clients/perf_analyzer + +function check_perf_analyzer_error { + ERROR_STRING="error | Request count: 0 | : 0 infer/sec" + CLIENT_RET="$1" + if [ ${CLIENT_RET} -ne 0 ]; then + cat ${CLIENT_LOG} + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat ${CLIENT_LOG} | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat ${CLIENT_LOG} + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi +} + +# Checks that the model infer/sec performance is equal to an expected value +# +/- some tolerance. +# $1: csv result file from PA run +# $2: expected infer/sec value +# $3: tolerance for expected value equality +function check_performance { + # get the boundary values based on the tolerance percentage + MIN=$(python3 -c "print(${2} * (1 - ${3}))") + MAX=$(python3 -c "print(${2} * (1 + ${3}))") + + # delete all but the 2nd line in the resulting file + # then get the 2nd column value which is the infer/sec measurement + report_val=$(sed '2!d' $1 | awk -F ',' {'print $2'}) + + # check if within tolerance + ret=$(python3 -c "print(${report_val} >= ${MIN} and ${report_val} <= ${MAX})") + if [ "$ret" = "False" ]; then + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi +} + +# Iterate over the grpc results to ensure gRPC times are greater than 0 +# $1: client log file +# example line: Avg gRPC time: 42648 usec (marshal 6 usec + response wait 42640 usec + unmarshal 2 usec) +function check_grpc_time { + grep "gRPC" $1 | awk '{print $4}' | while read -r line; do + if [ $line -eq 0 ]; then + RET=1 + fi + done +} + +# Create input_data.json to communicate the requested model delay +# $1: desired model delay +function create_input_data { + echo "{\"data\":[{\"INPUT0\" : [${1}]}]}" > input_data.json +} + +# Setup server +export CUDA_VISIBLE_DEVICES=0 +SERVER=/opt/tritonserver/bin/tritonserver +SERVER_ARGS="--model-repository=`pwd`/models" +SERVER_LOG="./inference_server.log" + +rm -f $SERVER_LOG $CLIENT_LOG +MODEL_DIR="./models" +rm -fr ${MODEL_DIR} && mkdir ${MODEL_DIR} +MODELS="ground_truth" + +for model in ${MODELS}; do + # Add version directory to each model if non-existent + mkdir -p "${MODEL_DIR}/${model}/1" + cp ../python_models/${model}/model.py ./models/${model}/1/model.py + cp ../python_models/${model}/config.pbtxt ./models/${model}/config.pbtxt +done + +# Run server +run_server +if [ "${SERVER_PID}" == "0" ]; then + echo -e "\n***\n*** Failed to start ${SERVER}\n***" + cat ${SERVER_LOG} + exit 1 +fi + +# Run perf_analyzer +set +e +RET=0 +PROTOCOLS="http grpc" +OUTPUT_FILE="results" +MODEL_DELAYS=(0.05 0.5) +TOLERANCE="0.05" + +for model_delay in ${MODEL_DELAYS[@]}; do + create_input_data ${model_delay} + EXPECTED_RESULT=$(python3 -c "print(1 / ${model_delay})") + for protocol in ${PROTOCOLS}; do + for model in ${MODELS}; do + echo "================================================================" + echo "[PERMUTATION] Protocol=${protocol} Model=${model}" + echo "================================================================" + + ${PERF_ANALYZER} -v -i ${protocol} --concurrency-range 2 --input-data input_data.json -m ${model} -f ${OUTPUT_FILE} | tee ${CLIENT_LOG} 2>&1 + check_perf_analyzer_error $? + + check_performance ${OUTPUT_FILE} ${EXPECTED_RESULT} ${TOLERANCE} + + if [ "${protocol}" == "grpc" ]; then + check_grpc_time ${CLIENT_LOG} + fi + done; + done; +done; + + +set -e + +# Cleanup +kill $SERVER_PID +wait $SERVER_PID + +if [ $RET -eq 0 ]; then + echo -e "\n***\n*** Test Passed\n***" +else + echo "=== START SERVER LOG ===" + cat ${SERVER_LOG} + echo "=== END SERVER LOG ===" + echo "=== START CLIENT LOG ===" + cat ${CLIENT_LOG} + echo "=== END CLIENT LOG ===" + echo -e "\n***\n*** Test FAILED\n***" +fi + +exit ${RET} diff --git a/qa/L0_perf_analyzer_report/test.sh b/qa/L0_perf_analyzer_report/test.sh new file mode 100755 index 0000000000..469d11ce3a --- /dev/null +++ b/qa/L0_perf_analyzer_report/test.sh @@ -0,0 +1,175 @@ +#!/bin/bash +# Copyright 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +REPO_VERSION=${NVIDIA_TRITON_SERVER_VERSION} +if [ "$#" -ge 1 ]; then + REPO_VERSION=$1 +fi +if [ -z "${REPO_VERSION}" ]; then + echo -e "Repository version must be specified" + echo -e "\n***\n*** Test Failed\n***" + exit 1 +fi +if [ ! -z "$TEST_REPO_ARCH" ]; then + REPO_VERSION=${REPO_VERSION}_${TEST_REPO_ARCH} +fi + +source ../common/util.sh + +# Setup client/perf_analyzer +CLIENT_LOG="./perf_analyzer.log" +PERF_ANALYZER=../clients/perf_analyzer + +function check_perf_analyzer_error { + ERROR_STRING="error | Request count: 0 | : 0 infer/sec" + CLIENT_RET="$1" + if [ ${CLIENT_RET} -ne 0 ]; then + cat ${CLIENT_LOG} + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + if [ $(cat ${CLIENT_LOG} | grep "${ERROR_STRING}" | wc -l) -ne 0 ]; then + cat ${CLIENT_LOG} + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi +} + +function check_cache_output { + # Validate cache info in perf_analyzer output + CACHE_STRING="Cache hit count" + if [ $(cat ${CLIENT_LOG} | grep -i "${CACHE_STRING}" | wc -l) -eq 0 ]; then + cat ${CLIENT_LOG} + echo "ERROR: No cache hit count found in output" + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi + + # Validate non-zero number of cache hits + ERROR_STRING="Cache hit count: 0" + num_cache_hit_lines=$(cat ${CLIENT_LOG} | grep -i "${CACHE_STRING}" | wc -l) + num_cache_hit_zero_lines=$(cat ${CLIENT_LOG} | grep -i "${ERROR_STRING}" | wc -l) + if [ ${num_cache_hit_zero_lines} -eq ${num_cache_hit_lines} ]; then + cat ${CLIENT_LOG} + echo "ERROR: All cache hit counts were zero, expected a non-zero number of cache hits" + echo -e "\n***\n*** Test Failed\n***" + RET=1 + fi +} + +# Setup server +export CUDA_VISIBLE_DEVICES=0 +SERVER=/opt/tritonserver/bin/tritonserver +# --response-cache-byte-size must be non-zero to test models with cache enabled +SERVER_ARGS="--model-repository=`pwd`/models --response-cache-byte-size=8192" +SERVER_LOG="./inference_server.log" + +# Setup model repository from existing qa_model_repository +rm -f $SERVER_LOG $CLIENT_LOG +MODEL_DIR="./models" +rm -fr ${MODEL_DIR} && mkdir ${MODEL_DIR} +ENSEMBLE_MODEL="simple_onnx_float32_float32_float32" +COMPOSING_MODEL="onnx_float32_float32_float32" +ENSEMBLE_MODEL_CACHE_ENABLED="${ENSEMBLE_MODEL}_cache_enabled" +ENSEMBLE_MODEL_CACHE_DISABLED="${ENSEMBLE_MODEL}_cache_disabled" +COMPOSING_MODEL_CACHE_ENABLED="${COMPOSING_MODEL}_cache_enabled" +COMPOSING_MODEL_CACHE_DISABLED="${COMPOSING_MODEL}_cache_disabled" +MODELS="${ENSEMBLE_MODEL_CACHE_ENABLED} ${ENSEMBLE_MODEL_CACHE_DISABLED} ${COMPOSING_MODEL_CACHE_ENABLED} ${COMPOSING_MODEL_CACHE_DISABLED}" + +## Setup ensemble models, one with cache enabled and one with cache disabled +cp -r "/data/inferenceserver/${REPO_VERSION}/qa_ensemble_model_repository/qa_model_repository/${ENSEMBLE_MODEL}" "${MODEL_DIR}/${ENSEMBLE_MODEL_CACHE_ENABLED}" +cp -r "/data/inferenceserver/${REPO_VERSION}/qa_ensemble_model_repository/qa_model_repository/${ENSEMBLE_MODEL}" "${MODEL_DIR}/${ENSEMBLE_MODEL_CACHE_DISABLED}" + +## Setup composing models, one with cache enabled and one with cache disabled +cp -r "/data/inferenceserver/${REPO_VERSION}/qa_model_repository/${COMPOSING_MODEL}" "${MODEL_DIR}/${COMPOSING_MODEL_CACHE_ENABLED}" +cp -r "/data/inferenceserver/${REPO_VERSION}/qa_model_repository/${COMPOSING_MODEL}" "${MODEL_DIR}/${COMPOSING_MODEL_CACHE_DISABLED}" + +for model in ${MODELS}; do + # Remove "name" line from each config to use directory name for simplicity + sed -i "/^name:/d" "${MODEL_DIR}/${model}/config.pbtxt" + # Add version directory to each model if non-existent + mkdir -p "${MODEL_DIR}/${model}/1" +done + +## Update "model_name" lines in each ensemble model config ensemble steps +sed -i "s/${COMPOSING_MODEL}/${COMPOSING_MODEL_CACHE_ENABLED}/g" "${MODEL_DIR}/${ENSEMBLE_MODEL_CACHE_ENABLED}/config.pbtxt" +sed -i "s/${COMPOSING_MODEL}/${COMPOSING_MODEL_CACHE_DISABLED}/g" "${MODEL_DIR}/${ENSEMBLE_MODEL_CACHE_DISABLED}/config.pbtxt" + +## Append cache config to each model config +echo -e "response_cache { enable: True }" >> "${MODEL_DIR}/${ENSEMBLE_MODEL_CACHE_ENABLED}/config.pbtxt" +echo -e "response_cache { enable: False }" >> "${MODEL_DIR}/${ENSEMBLE_MODEL_CACHE_DISABLED}/config.pbtxt" +echo -e "response_cache { enable: True }" >> "${MODEL_DIR}/${COMPOSING_MODEL_CACHE_ENABLED}/config.pbtxt" +echo -e "response_cache { enable: False }" >> "${MODEL_DIR}/${COMPOSING_MODEL_CACHE_DISABLED}/config.pbtxt" +# Force CPU memory for composing models since cache doesn't currently support GPU memory +echo -e "instance_group [{ kind: KIND_CPU, count: 1 }]" >> "${MODEL_DIR}/${COMPOSING_MODEL_CACHE_ENABLED}/config.pbtxt" +echo -e "instance_group [{ kind: KIND_CPU, count: 1 }]" >> "${MODEL_DIR}/${COMPOSING_MODEL_CACHE_DISABLED}/config.pbtxt" + +# Run server +run_server +if [ "${SERVER_PID}" == "0" ]; then + echo -e "\n***\n*** Failed to start ${SERVER}\n***" + cat ${SERVER_LOG} + exit 1 +fi + +# Run perf_analyzer +set +e +RET=0 +PROTOCOLS="http grpc" +STABILITY_THRESHOLD="15" +for protocol in ${PROTOCOLS}; do + for model in ${MODELS}; do + echo "================================================================" + echo "[PERMUTATION] Protocol=${protocol} Model=${model}" + echo "================================================================" + + ${PERF_ANALYZER} -v -i ${protocol} -m ${model} -s ${STABILITY_THRESHOLD} | tee ${CLIENT_LOG} 2>&1 + check_perf_analyzer_error $? + + # Check response cache outputs + if [[ ${model} == *"cache_enabled"* ]]; then + check_cache_output + fi + done; +done; +set -e + +# Cleanup +kill $SERVER_PID +wait $SERVER_PID + + +if [ $RET -eq 0 ]; then + echo -e "\n***\n*** Test Passed\n***" +else + echo "=== START SERVER LOG ===" + cat ${SERVER_LOG} + echo "=== END SERVER LOG ===" + echo -e "\n***\n*** Test FAILED\n***" +fi + +exit ${RET} diff --git a/qa/L0_backend_python/model_readiness/test_models/is_ready_fn_returns_true_decoupled/config.pbtxt b/qa/L0_perf_analyzer_unit_tests/test.sh old mode 100644 new mode 100755 similarity index 75% rename from qa/L0_backend_python/model_readiness/test_models/is_ready_fn_returns_true_decoupled/config.pbtxt rename to qa/L0_perf_analyzer_unit_tests/test.sh index 1537b804ab..f2a70d23ff --- a/qa/L0_backend_python/model_readiness/test_models/is_ready_fn_returns_true_decoupled/config.pbtxt +++ b/qa/L0_perf_analyzer_unit_tests/test.sh @@ -1,4 +1,5 @@ -# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +#!/bin/bash +# Copyright 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -24,34 +25,26 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +TEST_LOG="./perf_analyzer_unit_tests.log" +PERF_ANALYZER_UNIT_TESTS=../clients/perf_analyzer_unit_tests -backend: "python" -max_batch_size: 1 +RET=0 -input [ - { - name: "IN" - data_type: TYPE_INT32 - dims: [ 1 ] - } -] +rm -f $TEST_LOG -output [ - { - name: "OUT" - data_type: TYPE_INT32 - dims: [ 1 ] - } -] +set +e +$PERF_ANALYZER_UNIT_TESTS >> $TEST_LOG 2>&1 +if [ $? -ne 0 ]; then + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi +set -e -instance_group [ - { - count: 1 - kind: KIND_CPU - } -] - -model_transaction_policy { - decoupled: true -} +if [ $RET -eq 0 ]; then + echo -e "\n***\n*** Test Passed\n***" +else + cat $TEST_LOG + echo -e "\n***\n*** Test FAILED\n***" +fi +exit $RET diff --git a/qa/L0_perf_deeprecommender/run_test.sh b/qa/L0_perf_deeprecommender/run_test.sh index f973d7fea4..434803e7a9 100755 --- a/qa/L0_perf_deeprecommender/run_test.sh +++ b/qa/L0_perf_deeprecommender/run_test.sh @@ -29,9 +29,7 @@ STATIC_BATCH_SIZES=${STATIC_BATCH_SIZES:=1} DYNAMIC_BATCH_SIZES=${DYNAMIC_BATCH_SIZES:=1} INSTANCE_COUNTS=${INSTANCE_COUNTS:=1} -pip3 install perf_analyzer - -PERF_CLIENT=perf_analyzer +PERF_CLIENT=../clients/perf_client REPORTER=../common/reporter.py SERVER=/opt/tritonserver/bin/tritonserver diff --git a/qa/L0_perf_deeprecommender/test.sh b/qa/L0_perf_deeprecommender/test.sh index bfc950aa2b..dc61d56e98 100755 --- a/qa/L0_perf_deeprecommender/test.sh +++ b/qa/L0_perf_deeprecommender/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -88,9 +88,24 @@ if [ $? -ne 0 ]; then fi rm tensorrt_models/deeprecommender_plan/model.onnx +OPTIMIZED_MODEL_NAMES="deeprecommender_graphdef_trt" + +# Create optimized models (TF-TRT and ONNX-TRT) +rm -fr optimized_model_store && mkdir optimized_model_store +for MODEL_NAME in $OPTIMIZED_MODEL_NAMES; do + BASE_MODEL=$(echo ${MODEL_NAME} | cut -d '_' -f 1,2) + cp -r $REPODIR/perf_model_store/${BASE_MODEL} optimized_model_store/${MODEL_NAME} + CONFIG_PATH="optimized_model_store/${MODEL_NAME}/config.pbtxt" + sed -i "s/^name: \"${BASE_MODEL}\"/name: \"${MODEL_NAME}\"/" ${CONFIG_PATH} + echo "optimization { execution_accelerators {" >> ${CONFIG_PATH} + echo "gpu_execution_accelerator : [ {" >> ${CONFIG_PATH} + echo "name : \"tensorrt\" " >> ${CONFIG_PATH} + echo "} ]" >> ${CONFIG_PATH} + echo "}}" >> ${CONFIG_PATH} +done # Tests with each model -for FRAMEWORK in plan onnx libtorch; do +for FRAMEWORK in graphdef plan graphdef_trt onnx libtorch; do MODEL_NAME=${MODEL}_${FRAMEWORK} if [ "$FRAMEWORK" == "plan" ]; then REPO=`pwd`/tensorrt_models @@ -158,7 +173,7 @@ fi rm tensorrt_models/deeprecommender_plan/model.onnx # Tests with each model -for FRAMEWORK in plan onnx libtorch; do +for FRAMEWORK in graphdef plan graphdef_trt onnx libtorch; do MODEL_NAME=${MODEL}_${FRAMEWORK} if [ "$FRAMEWORK" == "plan" ]; then REPO=`pwd`/tensorrt_models diff --git a/qa/L0_perf_kaldi/test.sh b/qa/L0_perf_kaldi/test.sh index 79f5edf7e3..31d4c99ee6 100755 --- a/qa/L0_perf_kaldi/test.sh +++ b/qa/L0_perf_kaldi/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2020-2025, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -48,7 +48,6 @@ pip3 install --upgrade wheel setuptools grpcio-tools # Build client library and kaldi perf client (cd triton-inference-server/build && \ - export CMAKE_POLICY_VERSION_MINIMUM=3.5 && \ cmake -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX:PATH=/workspace/install && \ make -j16 trtis-clients) diff --git a/qa/L0_perf_nomodel/run_test.sh b/qa/L0_perf_nomodel/run_test.sh index 7a89d5e011..fcfdf16e0e 100755 --- a/qa/L0_perf_nomodel/run_test.sh +++ b/qa/L0_perf_nomodel/run_test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -49,20 +49,15 @@ ARCH=${ARCH:="x86_64"} SERVER=${TRITON_DIR}/bin/tritonserver BACKEND_DIR=${TRITON_DIR}/backends MODEL_REPO="${PWD}/models" -PERF_CLIENT=perf_analyzer +PERF_CLIENT=../clients/perf_client SERVER_ARGS="--model-repository=${MODEL_REPO} --backend-directory=${BACKEND_DIR}" source ../common/util.sh -pip3 install perf_analyzer # DATADIR is already set in environment variable for aarch64 if [ "$ARCH" != "aarch64" ]; then DATADIR="/data/inferenceserver/${REPO_VERSION}" fi -if [ "$SHARED_MEMORY" != "none" ]; then - SERVER_ARGS="${SERVER_ARGS} --allow-client-shm=true" -fi - # Select the single GPU that will be available to the inference server export CUDA_VISIBLE_DEVICES=0 diff --git a/qa/L0_perf_resnet/run_test.sh b/qa/L0_perf_resnet/run_test.sh index 6823525d82..3e7b048c40 100755 --- a/qa/L0_perf_resnet/run_test.sh +++ b/qa/L0_perf_resnet/run_test.sh @@ -53,10 +53,8 @@ rm -fr models && mkdir -p models && \ sed -i "s/^max_batch_size:.*/max_batch_size: ${MAX_BATCH}/" config.pbtxt && \ echo "instance_group [ { count: ${INSTANCE_CNT} }]") -pip3 install perf_analyzer - MEASUREMENT_WINDOW=5000 -PERF_CLIENT=perf_analyzer +PERF_CLIENT=../clients/perf_client # Onnx and onnx-trt models are very slow on Jetson. if [ "$ARCH" == "aarch64" ]; then if [ "$MODEL_FRAMEWORK" == "onnx" ] || [ "$MODEL_FRAMEWORK" == "onnx_trt" ]; then diff --git a/qa/L0_perf_resnet/test.sh b/qa/L0_perf_resnet/test.sh index 8a559ed053..11538db4ce 100755 --- a/qa/L0_perf_resnet/test.sh +++ b/qa/L0_perf_resnet/test.sh @@ -43,12 +43,15 @@ rm -f *.log *.csv *.tjson *.json PROTOCOLS="grpc http triton_c_api" TRT_MODEL_NAME="resnet50_fp32_plan" +TF_MODEL_NAME="resnet50v1.5_fp16_savedmodel" PYT_MODEL_NAME="resnet50_fp32_libtorch" ONNX_MODEL_NAME="resnet50_fp32_onnx" # The base model name should be the prefix to the # respective optimized model name. +TFTRT_MODEL_NAME="resnet50v1.5_fp16_savedmodel_trt" ONNXTRT_MODEL_NAME="resnet50_fp32_onnx_trt" +TFAMP_MODEL_NAME="resnet50v1.5_fp16_savedmodel_amp" ARCH=${ARCH:="x86_64"} REPODIR=${REPODIR:="/data/inferenceserver/${REPO_VERSION}"} @@ -64,10 +67,15 @@ STATIC_BATCH=1 INSTANCE_CNT=1 CONCURRENCY=1 -MODEL_NAMES="${TRT_MODEL_NAME} ${ONNX_MODEL_NAME} ${PYT_MODEL_NAME}" - -OPTIMIZED_MODEL_NAMES="${ONNXTRT_MODEL_NAME}" +MODEL_NAMES="${TRT_MODEL_NAME} ${TF_MODEL_NAME} ${ONNX_MODEL_NAME} ${PYT_MODEL_NAME}" +# Disable TF-TRT test on Jetson due to Segfault +# Disable ORT-TRT test on Jetson due to support being disabled +if [ "$ARCH" == "aarch64" ]; then + OPTIMIZED_MODEL_NAMES="${TFAMP_MODEL_NAME}" +else + OPTIMIZED_MODEL_NAMES="${TFTRT_MODEL_NAME} ${TFAMP_MODEL_NAME} ${ONNXTRT_MODEL_NAME}" +fi # Create optimized models rm -fr optimized_model_store && mkdir optimized_model_store @@ -78,15 +86,21 @@ for MODEL_NAME in $OPTIMIZED_MODEL_NAMES; do sed -i "s/^name: \"${BASE_MODEL}\"/name: \"${MODEL_NAME}\"/" ${CONFIG_PATH} echo "optimization { execution_accelerators {" >> ${CONFIG_PATH} echo "gpu_execution_accelerator : [ {" >> ${CONFIG_PATH} - echo "name : \"tensorrt\" " >> ${CONFIG_PATH} - - if [ "${MODEL_NAME}" = "${ONNXTRT_MODEL_NAME}" ] ; then - echo "parameters { key: \"precision_mode\" value: \"FP16\" }" >> ${CONFIG_PATH} - echo "parameters { key: \"max_workspace_size_bytes\" value: \"1073741824\" }" >> ${CONFIG_PATH} - echo "parameters { key: \"trt_engine_cache_enable\" value: \"1\" }" >> ${CONFIG_PATH} - echo "parameters { key: \"trt_engine_cache_path\" value: \"${CACHE_PATH}\" } " >> ${CONFIG_PATH} + if [ "${MODEL_NAME}" = "${TFAMP_MODEL_NAME}" ] ; then + echo "name : \"auto_mixed_precision\" " >> ${CONFIG_PATH} + else + echo "name : \"tensorrt\" " >> ${CONFIG_PATH} + if [ "${MODEL_NAME}" = "${TFTRT_MODEL_NAME}" ] ; then + echo "parameters { key: \"precision_mode\" value: \"FP16\" }" >> ${CONFIG_PATH} + fi + + if [ "${MODEL_NAME}" = "${ONNXTRT_MODEL_NAME}" ] ; then + echo "parameters { key: \"precision_mode\" value: \"FP16\" }" >> ${CONFIG_PATH} + echo "parameters { key: \"max_workspace_size_bytes\" value: \"1073741824\" }" >> ${CONFIG_PATH} + echo "parameters { key: \"trt_engine_cache_enable\" value: \"1\" }" >> ${CONFIG_PATH} + echo "parameters { key: \"trt_engine_cache_path\" value: \"${CACHE_PATH}\" } " >> ${CONFIG_PATH} + fi fi - echo "} ]" >> ${CONFIG_PATH} echo "}}" >> ${CONFIG_PATH} done @@ -199,4 +213,26 @@ for MODEL_NAME in $OPTIMIZED_MODEL_NAMES; do ARCH=${ARCH} \ bash -x run_test.sh done -done \ No newline at end of file +done + +# FIXME Disable the following due to +# https://jirasw.nvidia.com/browse/DLIS-2933. +# +# Needs this additional test configuration for comparing against TFS. +if [ "$ARCH" == "x86_64" ]; then + MODEL_NAME=${TF_MODEL_NAME} + REPO=$REPODIR/perf_model_store + STATIC_BATCH=128 + INSTANCE_CNT=1 + CONCURRENCY=1 + FRAMEWORK=$(echo ${MODEL_NAME} | cut -d '_' -f 3) + MODEL_NAME=${MODEL_NAME} \ + MODEL_FRAMEWORK=${FRAMEWORK} \ + MODEL_PATH="$REPO/${MODEL_NAME}" \ + STATIC_BATCH=${STATIC_BATCH} \ + PERF_CLIENT_PROTOCOL="grpc" \ + INSTANCE_CNT=${INSTANCE_CNT} \ + CONCURRENCY=${CONCURRENCY} \ + ARCH=${ARCH} \ + bash -x run_test.sh +fi diff --git a/qa/L0_perf_tensorrt_llm/test.sh b/qa/L0_perf_tensorrt_llm/test.sh index 5bf418c5c5..e74b01e568 100755 --- a/qa/L0_perf_tensorrt_llm/test.sh +++ b/qa/L0_perf_tensorrt_llm/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -35,7 +35,7 @@ MODEL_NAME="gpt2_tensorrt_llm" NAME="tensorrt_llm_benchmarking_test" MODEL_REPOSITORY="$(pwd)/triton_model_repo" TENSORRTLLM_BACKEND_DIR="/workspace/tensorrtllm_backend" -GPT_DIR="$TENSORRTLLM_BACKEND_DIR/tensorrt_llm/examples/models/core/gpt" +GPT_DIR="$TENSORRTLLM_BACKEND_DIR/tensorrt_llm/examples/gpt" TOKENIZER_DIR="$GPT_DIR/gpt2" ENGINES_DIR="${BASE_DIR}/engines/inflight_batcher_llm/${NUM_GPUS}-gpu" TRITON_DIR=${TRITON_DIR:="/opt/tritonserver"} @@ -43,7 +43,13 @@ SERVER=${TRITON_DIR}/bin/tritonserver BACKEND_DIR=${TRITON_DIR}/backends SERVER_LOG="${NAME}_server.log" SERVER_TIMEOUT=${SERVER_TIMEOUT:=120} -source ../common/trtllm_util.sh + +function clone_tensorrt_llm_backend_repo { + rm -rf $TENSORRTLLM_BACKEND_DIR && mkdir $TENSORRTLLM_BACKEND_DIR + apt-get update && apt-get install git-lfs -y --no-install-recommends + git clone --single-branch --depth=1 -b ${TENSORRTLLM_BACKEND_REPO_TAG} ${TRITON_REPO_ORG}/tensorrtllm_backend.git $TENSORRTLLM_BACKEND_DIR + cd $TENSORRTLLM_BACKEND_DIR && git lfs install && git submodule update --init --recursive +} # Update Open MPI to a version compatible with SLURM. function upgrade_openmpi { @@ -89,6 +95,125 @@ function upgrade_openmpi { mpirun --version } +function build_gpt2_base_model { + # Download weights from HuggingFace Transformers + cd ${GPT_DIR} && rm -rf gpt2 && git clone https://huggingface.co/gpt2-medium gpt2 && cd gpt2 + rm pytorch_model.bin model.safetensors + if ! wget -q https://huggingface.co/gpt2-medium/resolve/main/pytorch_model.bin; then + echo "Downloading pytorch_model.bin failed." + exit 1 + fi + cd ${GPT_DIR} + + # Convert weights from HF Tranformers to FT format + python3 convert_checkpoint.py --model_dir gpt2 --dtype float16 --tp_size ${NUM_GPUS} --output_dir "./c-model/gpt2/${NUM_GPUS}-gpu/" + cd ${BASE_DIR} +} + +function build_gpt2_tensorrt_engine { + # Build TensorRT engines + cd ${GPT_DIR} + trtllm-build --checkpoint_dir "./c-model/gpt2/${NUM_GPUS}-gpu/" \ + --gpt_attention_plugin float16 \ + --remove_input_padding enable \ + --paged_kv_cache enable \ + --gemm_plugin float16 \ + --workers "${NUM_GPUS}" \ + --output_dir "${ENGINES_DIR}" + + cd ${BASE_DIR} +} + +function replace_config_tags { + tag_to_replace="${1}" + new_value="${2}" + config_file_path="${3}" + sed -i "s|${tag_to_replace}|${new_value}|g" ${config_file_path} +} + +function prepare_model_repository { + rm -rf ${MODEL_REPOSITORY} && mkdir ${MODEL_REPOSITORY} + cp -r ${TENSORRTLLM_BACKEND_DIR}/all_models/inflight_batcher_llm/* ${MODEL_REPOSITORY} + rm -rf ${MODEL_REPOSITORY}/tensorrt_llm_bls + mv "${MODEL_REPOSITORY}/ensemble" "${MODEL_REPOSITORY}/${MODEL_NAME}" + + replace_config_tags "model_version: -1" "model_version: 1" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" + replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" + replace_config_tags 'name: "ensemble"' "name: \"$MODEL_NAME\"" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" + + replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" + replace_config_tags '${preprocessing_instance_count}' '1' "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" + replace_config_tags '${tokenizer_dir}' "${TOKENIZER_DIR}/" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" + + replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" + replace_config_tags '${postprocessing_instance_count}' '1' "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" + replace_config_tags '${tokenizer_dir}' "${TOKENIZER_DIR}/" "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" + + replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${decoupled_mode}' 'true' "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${max_queue_delay_microseconds}' "1000000" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${batching_strategy}' 'inflight_fused_batching' "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${engine_dir}' "${ENGINES_DIR}" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${triton_backend}' "tensorrtllm" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" + replace_config_tags '${max_queue_size}' "0" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" +} + +# Wait until server health endpoint shows ready. Sets WAIT_RET to 0 on +# success, 1 on failure +function wait_for_server_ready() { + local wait_time_secs="${1:-30}" + shift + local spids=("$@") + + WAIT_RET=0 + + for _ in $(seq "$wait_time_secs"); do + for pid in "${spids[@]}"; do + if ! kill -0 "$pid" >/dev/null 2>&1; then + echo "=== Server not running." + WAIT_RET=1 + return + fi + done + + sleep 1 + + if curl -s --fail localhost:8000/v2/health/ready && + curl -s --fail -w "%{http_code}" -o /dev/null -d '{"log_verbose_level":1}' localhost:8000/v2/logging; then + return + fi + done + + echo "=== Timeout $wait_time_secs secs. Server not ready." + WAIT_RET=1 +} + +function run_server { + python3 ${TENSORRTLLM_BACKEND_DIR}/scripts/launch_triton_server.py --world_size="${NUM_GPUS}" --model_repo="${MODEL_REPOSITORY}" >${SERVER_LOG} 2>&1 & + sleep 2 # allow time to obtain the pid(s) + # Read PIDs into an array, trimming whitespaces + readarray -t SERVER_PID < <(pgrep "tritonserver") + + wait_for_server_ready ${SERVER_TIMEOUT} "${SERVER_PID[@]}" + if [ "$WAIT_RET" != "0" ]; then + # Cleanup + kill "${SERVER_PID[@]}" >/dev/null 2>&1 || true + echo -e "\n***\n*** Failed to start $SERVER\n***" + cat $SERVER_LOG + exit 1 + fi +} + +function kill_server { + pgrep tritonserver | xargs kill -SIGINT + for pid in "${SERVER_PID[@]}"; do + echo "Waiting for proc ${pid} to terminate..." + while kill -0 $pid >/dev/null 2>&1; do + sleep 1 + done + done +} + upgrade_openmpi clone_tensorrt_llm_backend_repo build_gpt2_base_model diff --git a/qa/L0_perf_vllm/test.sh b/qa/L0_perf_vllm/test.sh index 263a4fc3db..e1ce8cf2ed 100755 --- a/qa/L0_perf_vllm/test.sh +++ b/qa/L0_perf_vllm/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -41,13 +41,13 @@ SERVER_ARGS="--model-repository=${MODEL_REPO} --backend-directory=${BACKEND_DIR} export CUDA_VISIBLE_DEVICES=${CUDA_VISIBLE_DEVICES:=0} EXPORT_FILE=profile-export-vllm-model.json -pip3 install tritonclient[perf_analyzer] - +pip3 install tritonclient rm -rf $MODEL_REPO $EXPORT_FILE *.tjson *.json *.csv mkdir -p $MODEL_REPO/$MODEL_NAME/1 echo '{ "model":"gpt2", + "disable_log_requests": "true", "gpu_memory_utilization": 0.5 }' >$MODEL_REPO/$MODEL_NAME/1/model.json diff --git a/qa/L0_pinned_memory/test.sh b/qa/L0_pinned_memory/test.sh index beb16cd132..48ec05b84c 100755 --- a/qa/L0_pinned_memory/test.sh +++ b/qa/L0_pinned_memory/test.sh @@ -38,12 +38,10 @@ if [ ! -z "$TEST_REPO_ARCH" ]; then REPO_VERSION=${REPO_VERSION}_${TEST_REPO_ARCH} fi -pip3 install perf_analyzer - # Use "--request-count" throughout the test to PA stability criteria and # reduce flaky failures from PA unstable measurements. REQUEST_COUNT=10 -CLIENT=perf_analyzer +CLIENT=../clients/perf_client # Only use libtorch as it accepts GPU I/O and it can handle variable shape BACKENDS=${BACKENDS:="libtorch"} diff --git a/qa/L0_query/test.sh b/qa/L0_query/test.sh index ac9e3a4d3d..153cd69381 100755 --- a/qa/L0_query/test.sh +++ b/qa/L0_query/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2021, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -69,7 +69,7 @@ export TEST_FAIL_WITH_QUERY_RESULT=1 export TEST_BYTE_SIZE=4 SERVER=/opt/tritonserver/bin/tritonserver -SERVER_ARGS="--model-repository=`pwd`/models --allow-client-shm=true" +SERVER_ARGS="--model-repository=`pwd`/models" SERVER_LOG="./inference_server.log" run_server if [ "$SERVER_PID" == "0" ]; then diff --git a/qa/L0_request_cancellation/grpc_cancellation_test.py b/qa/L0_request_cancellation/grpc_cancellation_test.py index 253c23ab02..7d9331d5f5 100755 --- a/qa/L0_request_cancellation/grpc_cancellation_test.py +++ b/qa/L0_request_cancellation/grpc_cancellation_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -202,34 +202,12 @@ def test_grpc_async_infer_response_complete_during_cancellation(self): ) # ensure the cancellation is processed self._assert_callback_cancelled() - def test_grpc_async_infer_cancellation_before_finish_0(self): - # First version of test_grpc_async_infer_cancellation_before_finish - # Cancellation notification is processed before the final response state. + def test_grpc_async_infer_cancellation_during_response_complete(self): # long test - self.test_duration_delta = 2 + self.test_duration_delta = 2.5 delay_notification_sec = ( int(os.getenv("TRITONSERVER_DELAY_GRPC_NOTIFICATION")) / 1000 ) - future = self._client.async_infer( - model_name=self._model_name, - inputs=self._inputs, - callback=self._callback, - outputs=self._outputs, - ) - # ensure the cancellation is received between InferResponseComplete checking cancellation and Finish - time.sleep(self._model_delay + 2) - future.cancel() - time.sleep(delay_notification_sec + 1) # ensure the cancellation is processed - self._assert_callback_cancelled() - - def test_grpc_async_infer_cancellation_before_finish_1(self): - # Second version of test_grpc_async_infer_cancellation_before_finish - # Cancellation notification is processed after the final response state. - # long test - self.test_duration_delta = 2 - delay_process_entry_sec = ( - int(os.getenv("TRITONSERVER_DELAY_GRPC_PROCESS_ENTRY")) / 1000 - ) delay_response_completion_sec = ( int(os.getenv("TRITONSERVER_DELAY_RESPONSE_COMPLETION")) / 1000 ) @@ -240,38 +218,13 @@ def test_grpc_async_infer_cancellation_before_finish_1(self): outputs=self._outputs, ) # ensure the cancellation is received between InferResponseComplete checking cancellation and Finish - time.sleep(self._model_delay + delay_process_entry_sec + 2) + time.sleep(self._model_delay + 2) future.cancel() time.sleep( - delay_response_completion_sec + delay_notification_sec + delay_response_completion_sec ) # ensure the cancellation is processed self._assert_callback_cancelled() - def test_grpc_async_infer_cancellation_before_response_complete_and_process_after_final_response( - self, - ): - # Received cancellation before InferResponseComplete and the notification - # state is processed after processing final response state. - # long test - self.test_duration_delta = 2 - delay_notification_sec = ( - int(os.getenv("TRITONSERVER_DELAY_GRPC_NOTIFICATION")) / 1000 - ) - delay_response_complete_exec_sec = ( - int(os.getenv("TRITONSERVER_DELAY_RESPONSE_COMPLETE_EXEC")) / 1000 - ) - future = self._client.async_infer( - model_name=self._model_name, - inputs=self._inputs, - callback=self._callback, - outputs=self._outputs, - ) - # ensure the cancellation is received before InferResponseComplete checking cancellation - time.sleep(self._model_delay + 2) - future.cancel() - time.sleep(delay_notification_sec + 1) # ensure the cancellation is processed - self._assert_callback_cancelled() - if __name__ == "__main__": unittest.main() diff --git a/qa/L0_request_cancellation/test.sh b/qa/L0_request_cancellation/test.sh index 2b92c12027..7789f2006a 100755 --- a/qa/L0_request_cancellation/test.sh +++ b/qa/L0_request_cancellation/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -42,10 +42,8 @@ export CUDA_VISIBLE_DEVICES=0 SERVER=/opt/tritonserver/bin/tritonserver source ../common/util.sh -CANCEL_LOG_LINE="Cancellation notification received for " RET=0 -rm -f *.log # # Unit tests @@ -68,7 +66,7 @@ if [ $? -ne 0 ]; then fi # -# Python gRPC cancellation tests +# gRPC cancellation tests # rm -rf models && mkdir models mkdir -p models/custom_identity_int32/1 && (cd models/custom_identity_int32 && \ @@ -86,9 +84,7 @@ for TEST_CASE in "test_grpc_async_infer" \ "test_aio_grpc_stream_infer" \ "test_grpc_async_infer_cancellation_at_step_start" \ "test_grpc_async_infer_response_complete_during_cancellation" \ - "test_grpc_async_infer_cancellation_before_finish_0" \ - "test_grpc_async_infer_cancellation_before_finish_1" \ - "test_grpc_async_infer_cancellation_before_response_complete_and_process_after_final_response"; do + "test_grpc_async_infer_cancellation_during_response_complete"; do TEST_LOG="./grpc_cancellation_test.$TEST_CASE.log" SERVER_LOG="grpc_cancellation_test.$TEST_CASE.server.log" if [ "$TEST_CASE" == "test_grpc_async_infer_cancellation_at_step_start" ]; then @@ -96,18 +92,12 @@ for TEST_CASE in "test_grpc_async_infer" \ elif [ "$TEST_CASE" == "test_grpc_async_infer_response_complete_during_cancellation" ]; then export TRITONSERVER_DELAY_GRPC_NOTIFICATION=5000 export TRITONSERVER_DELAY_GRPC_ENQUEUE=5000 - elif [ "$TEST_CASE" == "test_grpc_async_infer_cancellation_before_finish_0" ]; then + elif [ "$TEST_CASE" == "test_grpc_async_infer_cancellation_during_response_complete" ]; then export TRITONSERVER_DELAY_GRPC_NOTIFICATION=5000 export TRITONSERVER_DELAY_RESPONSE_COMPLETION=5000 - elif [ "$TEST_CASE" == "test_grpc_async_infer_cancellation_before_finish_1" ]; then - export TRITONSERVER_DELAY_GRPC_PROCESS_ENTRY=1000 - export TRITONSERVER_DELAY_RESPONSE_COMPLETION=5000 - elif [ "$TEST_CASE" == "test_grpc_async_infer_cancellation_before_response_complete_and_process_after_final_response" ]; then - export TRITONSERVER_DELAY_GRPC_NOTIFICATION=5000 - export TRITONSERVER_DELAY_RESPONSE_COMPLETE_EXEC=5000 fi - SERVER_ARGS="--model-repository=`pwd`/models --log-verbose=2" + SERVER_ARGS="--model-repository=`pwd`/models --log-verbose=1" run_server if [ "$SERVER_PID" == "0" ]; then echo -e "\n***\n*** Failed to start $SERVER\n***" @@ -123,7 +113,7 @@ for TEST_CASE in "test_grpc_async_infer" \ RET=1 fi - count=$(grep -o "$CANCEL_LOG_LINE" $SERVER_LOG | wc -l) + count=$(grep -o "Cancellation notification received for" $SERVER_LOG | wc -l) if [ $count == 0 ]; then echo -e "\n***\n*** Cancellation not received by server on $TEST_CASE\n***" cat $SERVER_LOG @@ -133,23 +123,6 @@ for TEST_CASE in "test_grpc_async_infer" \ cat $SERVER_LOG RET=1 fi - - # Tests "test_grpc_async_infer" and "test_aio_grpc_async_infer" ends - # prematurely before state is released. - if [[ "$TEST_CASE" != "test_grpc_async_infer" && "$TEST_CASE" != "test_aio_grpc_async_infer" ]]; then - count=$(grep -o "StateRelease" $SERVER_LOG | wc -l) - state_released=${state_released:=1} - if [ $count == 0 ]; then - echo -e "\n***\n*** State not released by server on $TEST_CASE\n***" - cat $SERVER_LOG - RET=1 - elif [ $count -ne $state_released ]; then - echo -e "\n***\n*** Unexpected states released by server on $TEST_CASE. Expected $state_released but released $count.\n***" - cat $SERVER_LOG - RET=1 - fi - unset state_released - fi set -e kill $SERVER_PID @@ -160,89 +133,9 @@ for TEST_CASE in "test_grpc_async_infer" \ elif [ "$TEST_CASE" == "test_grpc_async_infer_response_complete_during_cancellation" ]; then unset TRITONSERVER_DELAY_GRPC_NOTIFICATION unset TRITONSERVER_DELAY_GRPC_ENQUEUE - elif [ "$TEST_CASE" == "test_grpc_async_infer_cancellation_before_finish_0" ]; then + elif [ "$TEST_CASE" == "test_grpc_async_infer_cancellation_during_response_complete" ]; then unset TRITONSERVER_DELAY_GRPC_NOTIFICATION unset TRITONSERVER_DELAY_RESPONSE_COMPLETION - elif [ "$TEST_CASE" == "test_grpc_async_infer_cancellation_before_finish_1" ]; then - unset TRITONSERVER_DELAY_GRPC_PROCESS_ENTRY - unset TRITONSERVER_DELAY_RESPONSE_COMPLETION - elif [ "$TEST_CASE" == "test_grpc_async_infer_cancellation_before_response_complete_and_process_after_final_response" ]; then - unset TRITONSERVER_DELAY_GRPC_NOTIFICATION - unset TRITONSERVER_DELAY_RESPONSE_COMPLETE_EXEC - fi -done - -# -# C++ gRPC cancellation tests -# -# allow_timeout_override disables queue prefetching, keeping requests queued -# long enough for the "Queued" cancellation tests to cancel them before -# forwarding to the rate limiter. This saves overall test time. -cat >> models/custom_identity_int32/config.pbtxt <<'EOF' -dynamic_batching { - default_queue_policy { - allow_timeout_override: true - } -} -EOF - -GRPC_CANCELLATION_TEST_CPP=../clients/grpc_cancellation_test - -for ENTRY in "TestGrpcAsyncInferCancelExecutingRequest 1" \ - "TestGrpcAsyncInferCancelQueuedRequest 2" \ - "TestGrpcAsyncInferCancelAfterCompletionIsNoOp 0" \ - "TestGrpcAsyncInferWithoutContextStillCompletes 0" \ - "TestGrpcAsyncInferMultiCancelExecutingRequests 2" \ - "TestGrpcAsyncInferMultiCancelQueuedRequest 2" \ - "TestGrpcStreamInferCancelExecutingRequest 1" \ - "TestGrpcStreamInferCancelQueuedRequest 1" \ - "TestGrpcStreamCancelWithoutInfer 1" \ - "TestGrpcStreamCancelThenRestart 1"; do - read -r TEST_CASE EXPECTED_CANCEL_COUNT <<< "$ENTRY" - - TEST_LOG="./grpc_cancellation_test_cpp.$TEST_CASE.log" - SERVER_LOG="./grpc_cancellation_test_cpp.$TEST_CASE.server.log" - - # AsyncInferMulti fans out N concurrent requests; bump to 3 CPU instances - # so each can execute in parallel. Every other test uses the default - # single-instance config. - if [ "$TEST_CASE" == "TestGrpcAsyncInferMultiCancelExecutingRequests" ]; then - sed -i 's|instance_group .*|instance_group [{ count: 3, kind: KIND_CPU }]|' \ - models/custom_identity_int32/config.pbtxt - fi - - SERVER_ARGS="--model-repository=`pwd`/models --log-verbose=2" - run_server - if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 - fi - - set +e - LD_LIBRARY_PATH=/opt/tritonserver/lib:$LD_LIBRARY_PATH \ - $GRPC_CANCELLATION_TEST_CPP \ - --gtest_filter="GrpcCancellationTest.$TEST_CASE" > $TEST_LOG 2>&1 - if [ $? -ne 0 ]; then - echo -e "\n***\n*** C++ gRPC Cancellation Tests Failed on $TEST_CASE\n***" - cat $TEST_LOG - RET=1 - fi - - cancel_count=$(grep -c "$CANCEL_LOG_LINE" $SERVER_LOG || true) - if [ $cancel_count -ne $EXPECTED_CANCEL_COUNT ]; then - echo -e "\n***\n*** Unexpected cancellation count on $TEST_CASE. Expected $EXPECTED_CANCEL_COUNT but received $cancel_count.\n***" - cat $SERVER_LOG - RET=1 - fi - set -e - - kill $SERVER_PID - wait $SERVER_PID - - if [ "$TEST_CASE" == "TestGrpcAsyncInferMultiCancelExecutingRequests" ]; then - sed -i 's|instance_group .*|instance_group [{ kind: KIND_CPU }]|' \ - models/custom_identity_int32/config.pbtxt fi done diff --git a/qa/L0_response_cache/generate_random_data.py b/qa/L0_response_cache/generate_random_data.py deleted file mode 100755 index 27e8c76c39..0000000000 --- a/qa/L0_response_cache/generate_random_data.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import argparse -import json - -import numpy as np - - -def generate_input_data(num_inputs, batch_size, output_file): - data = {"data": []} - for _ in range(num_inputs): - input_data = np.random.rand(batch_size, 1024).astype(np.float32) - entry = {"INPUT0": input_data.flatten().tolist()} - data["data"].append(entry) - - with open(output_file, "w") as f: - json.dump(data, f) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Generate random input data for perf_analyzer." - ) - parser.add_argument( - "--num-inputs", type=int, help="Number of unique random inputs to generate." - ) - parser.add_argument("--batch-size", type=int, help="The batch size for each input.") - parser.add_argument( - "--output-file", type=str, help="The name of the output JSON file." - ) - args = parser.parse_args() - - generate_input_data(args.num_inputs, args.batch_size, args.output_file) - print(f"Successfully generated {args.num_inputs} inputs in '{args.output_file}'.") diff --git a/qa/L0_response_cache/response_cache_test b/qa/L0_response_cache/response_cache_test new file mode 100755 index 0000000000000000000000000000000000000000..d3807fac019cb0f4e6f1b8cdb36cc58add0faacb GIT binary patch literal 5033616 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

qQ4|UlhlCf;--xKf=gPN$Pd^{WwSgBO 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

EO}(^V=6cke_!rc>TQm_Mr#!^BxE9Y|U?< zc5u(ZyB*yBaDM-#4jy#yn1k0ic-+B@Kgb_Xz`;u$Jm}z+4jy&zgoF1uc-cq!fyq!P5@j%WIA{e-N2*|zWeA%FaZ);?w1haJ4fS|@G$!r$`y z*MDDK#;N}Fe8ip5K< z@9_v&{;MqhZ_B>j;&)rTY;d+-4_Q3K9OHT1;w{$qY^=9;PS#R`b%mmvBTo(FB>Y}Y4IY z^%s#;V$kC18>JL4v-s|opOD1|TRd#>JuDuvxcbX(DzVby0n0vW@gWwkviP4Y9G;`J6k&f?7$udsNF#m8GbVe#WF-fHm^ES|J@#Nurh zKgr@Li=S-qc8mYb;vE)0)#9BNue5mD;(xceXYtc4-fi*IE#71Ce^}hNQ+E8HVevwX zpJ{Qw#m};MvBjen4_JJn#Y-)Iw#9=MKgZ%_7C+bGA&XD4c-Z3qw0Okg=UKec;#C%p zTKs&AS6Tc5i^nYfFN@b${6dSzEq;;3>n(n<#hWdDiN#wi9;Ps>M?lueNx*#V@mXhs7_qc&Ei{ES|Rb6&Cj_ex=2`Eq;~7dn`W9;=V!I@ju<- zg%-cs;(m)?WAS2($1NVP_za7eTKrmz2Q5C+;$;@EwRp(l*I7Jl@#`%fv3R}3D=pq& z@u7EfAy zj>X$7eyhb(7QfBn?G|sbc!$Mrw|J+;@345<;&)oyv-n*W@3#0o7Vok6y%zWFoE`rO zix*mauEqTpzt7^u7Qf%(0gFFi@luP=vv|*OfyEORf5zgi7Jt^_ zNsG5xyv^dzSv+O&=PllD@r4%eu=pa2cUpY0#nTpFVsX#nFIc?W;!7>wWAT?P?%O3h z{$I9up~aV3+;8z$EM9E!^L5sg;@iL3QZt;-C->`Vt;%{0!V)3^u zUTN{SEgrRahsCQbzQW=$i@#&>8jHVc@wmm`vv|G5S6aN;;_q9$#o`}WJYn%pi?>?* zLyIRZ{*lGoEdH^@Qx;!k@pg-UYVi(>ueNxn#nTo~TYQbhJ&S*4@otNMZt)(AueG?Z zI6MBow0NP#*IC?eanIt#7SC8bVDYakUTX0!iw7$7XQ}bVT*TLJYw+; z7O%AUcNULY{CkU6S^Ni!$1MJ%#cM47lf~l}|Jma87Voimv&H{w@fM5!V)2BLBEUkm)N1^(9p|7(H&wZQ*c;D0UfzZUpk z3;eGI{{P+r-xuxiOL*qjh2h46eXcst7p_Z9-C|Qmc;?H6>aNfT(dey7PcZrqq{kS&4e8-VZ%cZJ(c6(8X!Q1^`x#wCdc*I!zJAhcj2=jOh0!~Z zUTXA?q~{yG6Y04|4WoY(*2Cylk|p-y8Z#uYm6R3dWF${BE8h;Ka-wsbP4IXMh_)D+vvSW&oFv#(vyuY zB|XvTeMnC*dSB9GjNXs*aHIDpJ;dl?qz4-P7t;NVK7jOw-*o+hq}Lcdob(E#4iUOBuQ7TI=@mvFNqVW#N0FXy^jOk!jUGpOw$VqEo?-Mcq$e94 zCOy&Ua?%rwK9=+tqmLs!+~^9@LyR6zdZ5wAlkR8q38XjtqU#?ay~gMXq*oYyBI%_@ zpG11T(I=ChYxF6kXB+)D(ld-cmGoqzD@ji@`tPJC7=0S)F-D(GdbrX5AU(wBGe{3K z`b^UOj6RF>hX3mNM@g?SdLro+MxRZ3snO?2LENe?mlBGLnmzL<1Bqc0)7p-0z0MtY6WlS!{I`cl$M zjh;e!zR^=j&o#Q5^lYOqBR#|D%Slf*x`y;bqpu)6!RRYVk1_fx(!-6OMtX?R(@76B z`fAesjJ}5ShM#r)ETAtB0a?DCej0qzL9i4qi-U;;U``HX3}eno=tj%(KnM`YV<9n z=NtWR(sPZTLwdH+x00S=^lhXk8{I;BqS3dLo?!GHq{kS2C+Xov-$iX`X@-QF?ufP6-M7jda2R(lb&z%1El8~J&*KkqaP$a!{~=dPd2)h^hBc{ zCOyIE|BxPI^dqE)8~rHhAx1w&dZ5vdlkR8q6Qno%pzEI`y~gPIq*oaIBC^l+o!AU(wBH%SjP`YqD^jDDN+hVOL!J4mlFdIjkfM!!RPsnPF}o^SMf zq~{vFlJsn&-zPo8=nqIwHoBAaM58|>J;CUYNRKi4W75No{)F@pqgRn0X!NI~`x(8O z^o9+({%O){j9x=}h0&jpUTXB`q~{y`1?jm)uO&U(=r2jnFnS&7$wqsmCmNk0J;CU& zNRKhPi}Y}#za~Az=x<05H2Pc8{fu5udPBFae>dqhMsFa!!szcvFE#pm(({e}f%IIX zeF#2cGla1~nJ<;g@lAd7nFQms9{VVC=M*l{7h|wEK4>bCB(*2CyM0&$| zu0Pxj*myqP4H)aA`vIVbPR!MVg&rjIc0v~jy)l~W??<7(5qhoAtAt)D^qWF26MC`G z3xs}5=y^ilE%Y3rn}n_tdb-fnLSH2GB%#j``V^tZ3q4Ng(L#?9dYI5dg&r*QAfdMt zxy+G*4gq|n#-9pb1x=H9dp{EO7E%Ze~PZIhJ zp-&NdywKx>9xe0;p@#`QROrD%4-$Gip$mlG_z%(lLVqLlTA^17y;A5mgK$|{V()4La!BimC!4NepBdWLN6A2fzXc$Jx}Png`OjHlhAcSPZzpc=!=A&B=i|V zpCa^lp~ne5TIdl%4-q zMdr;8Nj_xaKfV0-*&ALTjS=WB6>N+}a*5#1uY^MKk)-Cd8oz+*?OE~LtxRc+znI`m= z^%M(u-Hx+Pf~*w{>%8d?!cx#Utt1d`8e0;?xxIb35r4y(pZ*#{n^sa7uG=(qt8n9h zO%ZtSXj(}z>H=?ecR$J-#=Mm#Zzbh@0Q17y@*CWghk}-L_Ji-txryOAyi;%J6--y; z&~unB%%S6%&dZ^PFuf^<4qP`F#>eN(l^QhszipJ_dMdPI6 zaKkv?_>o^0)#4M_a_sl=#;MTT`~0rD^jpYMKJ(?Wu8*MamIT45qG7GqiH;htsth+y zsWPT7DHsA6G4OE(O97S{css#SOaLFp7}~YuNEPUk5AyHJq-WiaAS3;$#EVe zQt4=M8VGM0v?{&&*09VQ3BPA9gw4deCWD#B!hmd;8N~s1k1>16+W~$D&xPe9f2Uoi zn8xL{=(-FlhOReJ{%IwlijkYtz>kp8I^?j@A3?z#R)PcEZw9!ZY+fRp_1EKztt0a`8+i1Ghj9FpEOY1XR#{8KWnaV601ke~9f;PAu20oU0xel=T2gh%73c_zX@8~V zCXdm{Z1gC3q!ui!{wucNmFpnpc~AkYOnJl0-mcVZ)1fJ={boq*1o_eWLdwsBa1&(h zS_r)dLm)hIef6bWi@dAhJ7A1q?n@pp2ZR|hbx|YdF*Yjb%c!1>(zs{A@Ksqa1)8}E zIwjn=s05!_hMQ_j()bfrOk~@pQfV`+>g?ymH55ehHeSnwc0y@0^-s~t}d#>PsW8Cy3k8AdkH8n36vPi!Y{Fu zX(h3Wkw2@XxK2uN64Y;ONz}Adl=97_#6=}Zw1()4Rpyk=d=8|KuluH`?k~_JnS-fa ztR@%>$(bFwwQ%-eYvBhSv6u+flLSV%E0}hTHZ1{m)C6Fc+a4R%s+t7DuqKAg$oU8{ zH0Oc1b=tkK6cRF@!b_J}{U}zS_Rbhp?*VoO<7&0Nw9Eot4y%${Z|~U;8Xj`fxsy_L3tqgW-ba!;TBN?;Y#E< z#-JIz!8C7hwS{$1wb7OPVx!RN5>}*&$a*gU+g(M&CNI1Twqn83vBTlVpcnB+0}qwP zkQZ+FcmFg6QBZU=Y=sT$Ff{^0#Vdk#?t%N&PIAcPkPXN2j zSZG~X-j;;02S;O0Sh?oVCb+1i9`azrwctG3E6>K?#HGVAw~!^?%;dmpK_)9tgTT;x>0@!3A<`4Kz8l zM4E@3U+sw9D6zHURMa9h0kcE0L#IbMJ;C`)RsN(-4{>^&)A?v-8YV#L*wx^?*glVfCR>c!sJNxvr@8j|6H|ivz390Iu2j=uuoqvL%w_-I^=O315Ts){2 zxV0Z8OL*sFC}9|z_fQ|WPLdCSX+_fm>J$%SsW9C1C{3D*Cbjl@)d#A@xoLO&xyc&= zKe0oG(a+W1GMs)h)yRi+yM8LH+w21ypN0^0Dvv00)cuQ9aM!))@QqJlY||18q6m_; z8yyO+-@IGF%D#qY@Q1i#)cB@;nAr}BZuSO!L@n!mbIz8&B{f)iaF+t;?FU0hZ}s=m z*=1oGW}VuQmcW?+tM|-KxH&8gn}m6g0EdAfffS7EriB6cozE7(!@;6)rXN5>4IJ&4e?&ZbQr(z*A2cTy zCSb7ANeExvgIsYuOoSk4IjXerWpX)4+H2u1ji&j26v+)(|T_vc!6!X`h@ZZ*rpd(!g{pC4`F(LfE>8P zovn&#g++`CX``0=8l2-cITlj!l<9}luJ-}M0|>73*1=i2>mj(G_p^7{Y+Pp+H)DfZ zC!@y2ZA=Qh^FLNQ?}ksQ3B9d1pXINd1@~7tg5Xgtd;BPeXs`k^-p9ncVk~H6=hV~7 z8Oox#fLE6VF*1p0Cx4iZBkJ^3Zg4R z<3o9iHorj((W3bpiYx1G;}DI)uk&i$h@V?KFs5g2styG&Mc1 z080#g1#Ds1Fqs=S8~V;zFl4!hv;4G`>bo03Lx~X#V$|Xw6ih)ybx_-LQUBbG-IwpF zkPhFB-4eFjS=muTG^sYgJrqBYYPZQvV4nGUBlrJis##Qf7B<{|uxw{eAxER2PB>(X z>hJ>xxUXFOE?7JaU9QLGU^P;bWWOMVKWNNXAB-*gb^&#Q4D(#F&UT##UNa{E^ge~( zx|U(qlbbf%DKkD$ow7CU40b0J)&2;ThL$iLQHMpO3G%(jDM(7Oqx$H-@-K?XD&}TtBKyaZDfo4uzqXjF+bVjhBpP`{yhfe3`QbjJsaN%AY!Gvz4!XUsZlv zJQM7qhC{x=K8qqyD^G}u{n;ynpIEC7*qU@lo~#-%*F{$fcZ^pXF&cI0kmn-!mz6jv z7x5K$t-20@<%0Gmbx{J-vp>ZFSH)@$4l2eF?3+wm5?Sw!qOCN0NasU#2kv+{&lj%1 zCZ+qIo2FimC)~JBNhDi+5Kn7$vRV^`mTbL|Hl+0bOS-hfTUl>qk?y$-@urMp8_1+U3u!xq)H;)J;S zJ(!L6(0n-x?yTKalEQKu2X7pXH{I^)PjY!J)fQ49q=GvR#G7tE!xc5&cf%u*cN=WM z8Qc^wwi~v@Nm=j+dT1Q<2K`WX?Z{bU!NV%$KU4(A&iC~w^v0Y>rjS|hNVp^DPJhlF z*A$X9p{Z3nS?`+rgRp$Ae zd8s3FdHymF3<`E7R8H}qvkmAETqsJzyTh|Zr5Xr1P;r8vq=zhW^b-f2fU=8FGMGU z7~61AGw`ATmWAhNC;i!5alM-NK`}gO3NQDzfI4)gu;#672zNL5NNU2^4vXNM&H3CE zz_73+W}1pcY4%gHavRrCiT`ESdaDIR`AmsPJ<=SnpG~>tlPjma}ZFy zx?hIp5L~~)jZc@vvC8^ndLfu%dzSpZg)f5}bu*Utew|vs(y)41H}ASpV*CCT;{tzG zIenVFtS#`qcpDCTa0Q)!^L(D&p{Nc&V~lOQaJ@RFK1Dy*dyl}+aN}Vm-hK27-s)`O z--^8(`L}>q%fFR*SK@DKE1~8r@Y)62>d|8;)2`C3arPo*fky?} zW*OLI9z_ZjEu$cw_pH!$~0%@0BV~>CE@-!-)l=M@u%Km zNZ=wcoa64sDGpXXWd)OrqbGot12Fp2xfy6Sf;ok&otoG`Sq^oCRE#V;T*u-IQ&xmj z92N_`pF7GP{#7-mHRo5zUa5gHG7EcmUzs#K0S*7ehS(StBiF08HjS`9#tWTGjWBXO zRGtFh*1*dD0iB%AVbK_Xno#HK%A1j}`W5AjMgmw-_v3}#_8gfR4-3FD(_^D6bI&HFzZ@G=Sy-|?QmDl?&jL= zhI=QXyM4J2=S}akecoT5(>}X!ZB8JMI-WHz!BDMHdnt~K34Qy#HrMA%TdNlOmA9)t zTj(+<3J-d3u`##MHQ>W;p%cIiTS$%5rPKPX<9Dy*)bSvW=U@Mqc)om5#dCOWJnP{W zo*mB%VAiz==hFy|g-$%?bZZ~L&Vg_$nx0ng`{>6ZaNcUf%RlgN5d6d@c%61derpNV zoI>1k^vXm6$I3VikiEfiQ9so12JZ?Hw&DWVgQv>uLp76g8>-qS?|wKPpdDU9nwK=v zmAsYZgk_1C`KyY(~EIeX1!vp<+G`{A7Ze3*~WgZv<0J?QctNWtX! zm@RhMYyso)6|f3-85kO!S8T3&Tjk4s4`;u9v)Pxwl&5vh;_RnwHhX=(>;c*$d%wv? z1H_E8=`Uo*S>_Xe&QS}XoEv-!LVY@_xm5$NUj|ykhMI zv%oK}>u`C_riJJES1nN~TA~VcQ5}Ab3zkvl?L~Lj7pa?TFzHLx z8_sz3>uKLRfbXu$u%+tB>mq(bu(rf+FEJ!=3HWTWYJq%ZPrcc@Whqp#5#5dKBS<)b zTmXg`Weg9Aag0W~Q%d(< zO8a__toT;4+57l%2m1d7=zr*X8i;UxP?|XrLv?oB1?aA!%bfGo&Skxs@C0SI-lQml z9GTnubUFx~njpIHI;Ax88XnHoS)zKX8XJXTqVXc}Al##~R)cl8cfyF;6ZTTXWCi!{ z0h=@LpZoTBa<0cY?D0+5=FnrGr={@oYL!T^F%|!$Hvx;c&XYsHjL(zs3>zwp6OWdn zQW^vOARTM*cW^_e&H&yov~K>UpHl{9T#ignaeA?d14=2wQZjg|Eu&KI1f#AG!T+L? z1dJGbluKKH8dES7uS73U<)|qgRtn%}GY^^5bcFb^z~RCNvjnp52L~zbFO)|eG0dT+ zSr1e{rb30QU~hKE^Hp2^c-~kQLLE0f>8DP8T1Np%-|g>%nE>MgL=3$`NbO5h55OB9{KoccIG$?A$8liP^*r`|67G7-P#rl4ejrCs4x{BfnP>x1+G%w!4xVbfJdsJ8E4NyLbwTz zO5O=Sz(oNx$N+Yp?ZDNhQM%ryK+y%XM8Spq@xd8ha;oRl^ShqL^bn@=yNuO$cO|F| z%Wyn|%_G6<16_+hPT?Q;w_cNfVJLrKaF;*4j*-zii=)Bk|PLk1RdV3kg4lc3{#dljmvxwHz-KY zTgOwu2p{ZR@{4NUhqN&ckuR}nRa+c@#k`MI{P9!O&C|ist9(k;D?JrnUZd3zd~FG~ z-o@BaaOr~3+W=-b4a*^qdQh>HAY6eE11o zo+_pHn*79fLJl;We^G6OLov;K6ozsr z(an8)qu7oMii20v@A_TU%=F3+^TFM9MBQ<{4MrHZ6&D_Bv58DvkD#N`?8q{tcT;V{ zYtazT&NR0JIFzlqcrK*kkpF~QYrXfh+OGIhk*mD16bj5I%m?p%>vep8;9akHlakoT z>%FG8VaNxu8A13V&ASRZ8J}~#`|&_u*WG%YuZ5#Q*Ao;;O&Gea9<%6^E(z@Zx6vz1 z(uh}Gp-zB=yNpWA`dWrvmc)p$NQ@s>cAm3(PMWdN3F+8=LGm)Sq!>Hq zC4|oK=5cBq%fLmVsP=ds!OgnZ8DNts;}M)f+Nsl(C2yq)p^TI}I&pl2Z+8>k4kTgYgtcn>6G zDxiv{Csok7YhDdzhw%_KXBM@VMVW`pX)r?l6$WmfIPH0}Kjm?qfVam~2=t${Y4T?j z@X+!VhOM5x!+==#7}WOznhi1Yh$RkLgmO&I~HRc z#jzOp6p`z#fhfCZ0~hc@^3-j^jR%+D+&zXOV4u{M#^?T=1ovIw^ChRy=+ZB6_#rKG zr0$IX-Sy!8C#Y{NeEp~jdn|>$s3gDDD`15$^g!ro9}Zl;k<*O=r_?-j1F0zl*%jVQ zsX9(ia5`_2NtBp>5~uqhE?nWLl4gH622<4|c7H{~Pu?MH9cOPrv19H+>HI;6QZ-!E z5Zm~Os_rSU9jg8|bs=XM%ZFq`8y4-1@X7++T-L8FJcsF4UHPSaQ`g(Mj19aEHDi=8 z)gQ;fkU)W@OaZH|JJAn~D`?S1OmEfcCqa6qnpX=k^x#!VO0djJ(7c?@b&1r9ro?_2 z7mm!{RsQlnG{Z85?11uQyU}#h60plRb01_bXYP&4FJ=j3FOGuL0GCo8*UUW`Q(-zi zO8cI3<_-fhp1ENv01l=4LS`VQ!7;h_>sUWOtel^K&YK`M@5eXP{x%v2jf-MjH)*Yf zT?@MFWzMoYW`RTF%ba0b)=#tkjdq}Q-UF<^X8mv0pU^&r%>g5V{HxYDm;Eo|biYXt zQ2Apy{Vqp(38znUr1$6ae>l>=XjM&em?QmVPTw8U%i;AHTB7mGB-GbC%!6dyB{#y( zgDcjEh5hjef+x>?@mJ`CpjsDTq&7q zM{z4{a3WtnJ6}`I%DWXLWiE%ojj`SSJjLeChM#o)=WP@Mgq?xEjI5$hM#JMx`cr_EU?ns9HPG8DQPjfwhONk6+27h@uqH;pj{+ zJ-tp30Zzr)i^I1bU7NYP1*B#!g*Mn+#2-GWYB&7<6!9Zq-ABZ;(3vyhi#aua#1TlP zh_OB3;L8(IO_+l)EA&AlY;sg1KQo$)((0=M)YOPj z;sQ>DR>mt42&w{0g&1M2Y=OkC!?4$}U(u~N|50XFJf~qQbnKJ7U1jTSP0bN()MSiC zuC7jZeN7_)PRe>BtEvnjYx>H8kadbyRr%_ERsU>NtY!I4NW?X1<$V~`YHu)<=km{t3Uk??@HGL!+&t+o5&b!si1XZx zD61QerbLK-G)F%Nitu7{;lJH5`m3gx=yy}mU)YbLzX*TikN!KjYh_3O9q*8bQuO?6 zv5BAK=u2Zd5aJkr8p_7HZfjfXW3jQik*we-iEmFs<;uJZ+xq*uUctGc8ZrAxYnmgp z5u*Q-F(+7cV^h;58l~g;nC95QoZi9dg;U{U*|q9)j`??j^e*^#La)zd7u8-&3yQVr zcnN62N?vC2UZlSQ78rlAdLsS7m+HP;5%0Kp^W2=9=YG!fF>O+gn*nF$=DCFPJk(d7 zeRK1S;yhFO%Jb8`IrSO9d5-8S&x^Tv-d(HOU>nM#m%8=USJ7JEt9z>>c=R~_nw?=z z^`063MBgx)mhgTk4+*bh-?m{_W8ap6bypiAGhBuux*JhaTkQZSbIB%E(Y!s>eNRp+ z^y3vn_s9_DCjFseQuuHInN5%oUk$S#&&XUo=KeNZpcG#i0?F?X8e*$ zsIIp+7~@Iq8c6j{yIZy8iF4pl3npX37J!8pgRatdslLE~A)%tLAl^n-*n9$Ls#!%X(YPgwuWzTTj{(7q4;5jqT0N1p(By5L!meHuIylcSUz z#N^{KIlKAOqFF`B19MCK6PHN)E9IdJJS#)`E>&F$9d4QFV@f-4_3bARR_`YS4|6tN z9G?G7wbFOI9fdFt9E12=rZ(Mb&O@g<>(R~`SQOow!(GxG9n|$N4E@oRk5|(GY=z!# zPr*Qd>V(zObQqPs+M7zH^9yOPijxP30UmT&U(WLFolqRS9mCTVPAd0dx|vpQVfszp z0Qj+_kcZOqAiSY41oTk4ozEiuph!A@K>P9gkWhCBx*l?1h<G?umfE=dJb{uM+B5&rb(BXU`^Hz4~RzqUf5@;>)O@Lo?#+A|It#sF_2jDtx1t01@+Eiz9j<8D&pesCnjOE7 zdzSJus+S))9iT@H{CF(#8D}vEu)FKQ9e#*rJ3!YaavY z*cS;Hphb0Q80d7fZ*dfM54;=ahmW$xa3{f-o`+2MDQDE97UNe^QF=tD-=Wfdl=o*! z^Xs&$Ra!vh?a+CHI{i#ahc6x;wHQCkOV6{KsnoHU(p2XG4L8(bsSpGH@UHX~z^qyA zU3)Jw`EADAX7g1v{K{FoUNYZy{Pk_HOKy+lH%@O2H;sdDX<9z@0Z4A6V&8=1t~r!8 ziaw8C{YBSxP;w`fyad-2?-6vM8fWy|hrrXd3t*~c&H|6nqP9i{8BW6OU?gy!Sc^R_~igBI34duq^t#3G~#pL^2 z(JSsO=3*&I#eGBx{N-mjlZtBpM(xAzL*W>B25d4k0(2F@Nl|SH+hxzcXs24&dNZ#E zX%Dm@%y4+V3X{_t2Wu$JaCqF!_r+jOgOV!17Q+m9XAi)BUhmz(+gf%nT8$G%nGXhY z^~6179I4e|OLPR;PlxQ(GvbU@eIQLU?{sW0useu~(dW9=Q{l43I$SN71LbrgYSRsU zAEJfujP=zKFbdAanF}AO%R0!t2A&Gmm`eRb>y*`^%}p z^#T=uw9H@i$fJXxzV=S(D1ZAm4i~zUPvZ%S=KI$PZSo)hq%2T9c;^~X$aJ8 zi`_cvZ*NiMClKH~$nB)JIXO4Op)Vip40n9cWt)}Kb+zdtBOVm}w3NXRW z(M3Ft4$L!|ry#ZKPMS#(xF|I(AJtc=H*>pvP9=YOd<1mr-!K(M)8n|o zH5Od|eGt~n!S(n9F6Oa3eN%;ghnp&u!0;d3RN3{1puVv00P5{Z=Ys5|bP(5f>-$18 zv!N!lv8L#(cST->bW9CWDnI!$6?ueH{g8@F4vipFktR;1t~CQJO@Ts3U(Cr#1TYoL zRQ8lkKc3R@kqsnn5$P*JC{d86l2B#_hmDf zHS%c4-wH2*iPZ}5`Dk`xT4F9bDFs^5@MuXDx`swe;?HdGDP7`Ja)-xu_)-Bh_HL%5 zeZegosIQ-*7Nw{9en;98y`*`+w$hzjV+F+C;A0%(QWQsg-f}B zs8_Xjfv@X7n1j8jP<*x@xHnd zsyCh&oBRPR2NMV&vo~5nBV7L+yi1bA-=^bl59Um#JJ8=RFbrsIpa>ygKGdXiz*EsB z)VLWzwn7P=eln%=$d{YVA)F}BG(%n$5YR<~l{~09QCln`lZiCo)xB7!x4;1ngy4Y+)N- zdcgaw{0-~H^bPBO0xP^#eI;9c_AGQwC)#)7Z}c6JZ{ctBZfDRmQHZMa#VmN|0PbBp zgc%+}v%B%PyYaVM@V8s=w+8&J0e;I&gQ0+88j4F;T!P|k66?LQafU-YU(sIF`y%U7 zC=t_NbEKb$>DVdQvepUcQ04&~HaVAh0rptK9?QZ_z2BewwNVv&5*Xn{_d=Rn>%F5u z!+CVQ_a6oaI^ues9qiypzXQ|r#Cs4#}w>(7(ppVkfc4bcgvNreX}FI*b#xx z8Nrt~s75)QBY2P}%wHTOJj3?eJJN5(^gL~FHS^wWa4vgX1nVlcK?z6jC~b1R+u&ff zpXEs3$q~VC4JxwU5qyRzQ1YQ1!F@dI_H~r-B-{VNk$y9mB**WS&IqFHu?=Ppwn2a+ zXyv^!YA$KiLK0;AdPln75kXJAs|`NI6e#(x9Kk(2qxN!?@HpFVd-slUnxIaTOq*r4WKVvr8^ML9i6+u=$`M;`R+7$1)>r7RY zDZ!bFao>@*1ZQF*Os$V$O3fVQtKJF@V*MKq3eWWV71iAimG|b>Vww2Gr|>=7fpF75 z;8Rif3e7)2G(zp*kMZ>T&inXvh^GzqSp0WHct0#@X3zFT*PRAFo-e{~ZW{6AuR~!v z!1w)9^-d{-&&iNJ9yEH7gqtG%iiQjEa|dvYSW#403c0^8x+LT;+6V8}H--ye3Hjli zT>*`~JcReYLHKhE4nx7)6kWGB^&GVQ^V_P@$3p4e=TOgzCj1h0DvEB|tD?44MTMcL z3P|Rn%9~&}$MUMM9C~2N<(5Nh!fRY7msE{e-oFIC1;bYkAE2PdcHtE0cNPd4MzPo#0}uP>nSvV@2hPZy`lrW9L-*$+DnYSy3O(u!t+@Yy+B{V zJRXZ)5@dk&h48S-E73r027g8acxco6PT$s$g4}OTu`sy6<%Vn394P0lQl6$)KzhD> z&vCwioKK#%Z|8i$?489B6&S|^7?qv8FTpLlvC!Rc!<9ZirzX+qVAS;?nhkjjFSX$N z)yX{jVP9TA%}vIAxI7q2;>)FqxWMyl4^7!e`tS)A>$c>Y8Bj9ZY|q~MeO5RLp9eFvFLyENuBi54%3{4HS^+khJvQTL@7|8OcmXxB zZHbOn5_MPb)3~T$QSE)w(eq%FnZ=Geut6(fX4d#@W7Ts6NTzcp6iP#le7z^159N>o&eDge&BQ*mUr7 zETpOT8h^Fl^n3FfY!K|xgK!ps;cjF&&VyHDdeV5K=|Z@*-cZp$7+ww^Zh+`v{Vlk4 z4c?=GwOidX_HKntIFvud?Wn2X)KBpq2VeZcTBcrIr2~nn)=5}$Q$c?!1KzO#EAJ9I zz!$`TLGiQc2j1Sm(i;mVD|{*b0CQNOR9dLKk-{y9ggoK$`}){E&`ua-8ASO)5NhHb z4-;riTNr*F{?729;EydqM4!D4{+qU&^3zaQhx?bb!D{Zq#oHfVnjSLd6?g|vtD|(B6k{1ujN(eu)9C^J-vgR7>A-@2=^v5+4K2)%Af|?52nD zZ#>QEE+Esp!L%8ht{dE`BMUVf9iDJe+x==N0l&WO^H%jR^CC{lZ(tQBlTIuT3s?vyI3l9niVd*&z%8Jyac2iYUmQ^2J^vCu|6Uz#@NjRh zPJ6GywK9BBbGXsBDFR!@D`6;}!pHA#hUW3YU*j%zGQLpA_hOqy{07bI6&HwcF`lZ0PB!+DP<0XkUyFqKI&U-1`u+p1_jX|}~Y~;yHbG?=R{=NdG;ge^sJ_o~DQjAyfzD8c}L!LtB#)7dcgIMA+_^)K-K_nR9NuU&;j z!5h!;y)8HjyoLv1ZU2~WABxI}@pL2S^-jS&5B;C=oTl>(!8}ta4~*v>QBATv(1AU7 z{O+OiuAK+ghoCil1`zwp+Y*x+R#!B5nR9`UnK|9(t1fJ;C@CG&Fy03zxh;$8;9JOj z-XE{T`tnW+I~fPxXa9crNYtT}8vwB?-dDM2QvO^59ZP=)pZZw3+Xi(k zT~0gpDPcI4hHw#n7Io7s{(=siBoJ1(VV0UEd_uhd9Ki`S0>{RGD@!;zqEQcopwFo4 z(3*2j<&QJcQFS`_z)6O^rcbN4cEd~?0TEO*@W6ut;mk_MVtlLsg;qkLcd4O>h4M&1 zPwiMZ1c9>>4z5%_G#*_P>2otrTDbe10&6_LLgR(uCOKyI_RZkzS4EraH#1u+uE5@K zuXp=%dIDUTtKK9Qfs57lkm~&xcm6vEz>*6Oz`<1a}!WOTv=N>j$>Q7GT-+v~syJ-0+gOhK>ca zN*C4b^9u&F+S~nd{MpB1edm|G7wacrTzhAa{C`@k4_cS)cK13r9>dYyZ>^zq4?){| zdtwb>=ZSjbx(eVs?fXL?gXCAChA+mA$jj^ot>cZo4C@7dALIvX0bdW$I}Myrt3WZ- z9eu64i_uN*c*L@I8~VK#x;XPr587M(H4Tz7N9q1*c&Tf??(Mg%&@WrGg|B?(H*jS< z1sy=Y)K=q9-7io{+)2Id*Xquo1$xA{>!Bk)JmmkWBmP4^T=VY@EO)aNeh{p@*;gx5 zS`zT97$?G5#>$(s!!=-*IS#0|U@ERRKYOFU=k7oeMK7;|7h18A9FG~khYZ|E<*>u< z3kI3hS(Z8O=UyEMqra$jJK#Apz=z&`{)tXd^f55`@tV~T74`z$4as?avYet_i%G$7 z6MqVUZpnvenR$q7vnP%Y7-T==UIWI(-Wm78Q-d$y#738&e2XPOnJ&e`z!>*eJOzFR z(V>1GGpmmd?FqT@VcT*@!oOY%_YbDwb9VeBj(X@ZjGfd`iP@V~e4DESmpYvM1!xD| z*o+{%aVB*7+9{Zh7uHa{7#^Cg&t6)$Lt+LTkO$m4aK6$Cgx`&M4kcJa{B|@SZ=2(1 z>kpG=m!jET(rl14I}Xh@L3eYTY;zCn?CViOxbi;69$<`Zcp2{DO$9&SH5_7v2Sqbe zFkQCv4)|qjSP|BHr$TZwq<{CU9qjG7jry(7oA(R6=-gCrnJD1#|3Uy@TI{`t`&LuIDZ-!$4WNt#kc<(I z5a}0j`W#G$AeIzEcX)eWs;YfW4jst!{2co0WMz9&4qeN1w5Fx#_id(oYf7a($8=y0 zy`Sl?u+30pA@tD1bgic3_j0ChYf82gnLej!3_Tpj^nOhPNJlVj(lmv1Po|e^8brD^ z(}_9s`FrDd2O(}-+n7)uhCop|jQyP7vnBJl(c^Jxcx~61X#PmE( zDc=8HtZF<#Q;O_!rlT|^zi%=fswvq%%d|*SDs3*)|H5ibHKx)UnSQP**;X@sQ&X}% zi|MnPQViux=jPCZm^Nxkkqu^Attpi@fazJ9QfV75QteQ#DcOF^^dLXClRcyzdu#(DF?C zYg57Zx8lc~*6A%^{~y!>doq}#^>3dm>#?Yt3YO(s|NgzQ-b4bN1$>0IPq-QESd^VqFn2Be6u*$)0hj^|6HFJbLy=sb(h@K2&dMl)c?-O zsriANdK#oIse!vExR-tNFDmA(T+;z;dH|T>F7!lhviF^86~8KHc5doxocbMXKTB{y z!*c3WIsddx$HCvPC)?HDE4J<5!M5uill54XFBv%s<wwBVH#hf3-74%YRO-y!)J2^71#Uh~1(R}9@8Q%}cx$?tOL|6GAChYw zW9t^S4zYE;vfet^`bf6EK&5uwoYNvDoLZq$-^@+jic|l>CB45|wa6aI`rcgY&h@GV zd~Dsy*4@1QRGmpP{rucq_dzb48n3a{k;>|*T&rnp^#EEm6%5TyJ(EkmPNfdWO$~8s z6{gO-D&m`31V2J>+Y2u$wzv|ant4^3qNPNC_P)MC9de&OgJNDVJEzsY{Z`dp0KjkO%XAIhZ`}P0<+7 zQI4jbt$YwOe%&Da>90-R=M`9ypS?#8Qx*F2FmQpF3|_bGL=CUi?D_eq=72NMgu zu5an&zNk5;51-)sE`#L`rdxhdEC5cXcKy(iH7$CH%zgQWLsmG@hu_hXg! zM&bRXuT_h6UC6PM_v!3?B~m-~_b#W{>(-0dzdoH}A11s{V((4bdo#wqCwsqKdf)3R z@_xv7!uyTN``b6>w#)$ben5`*rRS=a*;RV4xthFZ;Js_?P1Ei{<$bd7{&1ISnH8`( zQLnaO?B}reg-Gp|d2I%Hzw>9|{hiaOWp)(ak7Mssw0CNm0``7}^uBu?c_06a@IF;} zU(l4>GT(fqVjq;_{UNvqfFlz3*PPfdt|#xCeiPnzR^Cq)-fv>>&!Qig_v(8J%h~%} z<^5vd zl&r%aRVi1gQnI~!B9_uraEtK%dPcR-_vhQaTgTp4DZ1V}8Ez_sXD&=a(Y~oW!*2=v zt$N#V6aD6$JC)LLXsGmhla9Z6VI!anuR!>xIa0pWPv$&?EcUImnSKhNNeLQ*xH%Av!OJ6TadBMZBSnsW?;ioft!w=(s%z$bQPEh9L5ORe&83h1&SK;_jJjJ{a`8k>gyjJGj=K()1%bS^ZB0os;Qm=-23i)oD z2fQfrR^(e|d6;=K@-FNvj3?j)nb#nHS98Bt%sh&GVV3)thmqf( zyv9A{GUO7)8$zjJQpw#Yl#&^w+2d5x`-(#J{r_vP=Q+=FE|c&3`2BQdulrhi?Y;Ki z&y8?zX1-V~y|r+oT`~#&;2}cGZST z>vxoVzgtlYTuu&^*A(wzzmP-H5_BuiHuVNV&U?RuN=*d!fu$z@d0Fgwv;EXT%Em^e$g7?5RS!K!~p|pO_+UbwMriN;%s~ znd+Cac)fM25w%BV^BVL>UgXMjGyW2Bbo)xLquFMztrl$s>;x^_G!D*Uw9I$=$s}`x zXmL06BUDRif|j;c%LQxvTF$XrX0ISPrlnN1Ote}S-RWES{_=zMt@VOUH|9Dq|7*^# zAjg#k&FY{vzwmR0E@pn{0x`d$+?Pb}es4qe6J`f*CVqIQ#GtbXiDhC7E2N)a$Yd4r zEQI(n@tNyH%FrKaN=!=iATi~yGM_1@s+7y(rMz8a`y=X?GTus=@0ar5@2o=yQ3{mG z%QYmQpC2xL(tRBRhC3x}ymC#`l_EYLc@RsrT$4%haXYV_=BBai6aW5bn;A)HByhZ# z;wJqlHRazZrQX<{XGi(%Tm1e!pD$4{rM{>fu0jSth|IO~Ib;64xwev|m}}$j0DSyn zL;_j*3##isHGdCKnNK+~{{cIH8D{Mq_z%i_N@f0rdh<`kbD+w6_8*k_+eOy*L5|G# z|9-z$fz-W8V=no4b@6$XxD!=TJpcceI8-IR9gGYPFz=&7PT5_t1=UlH8$jhz59 zfySKls2{sRvA?50L66zj9;A!|mG?|zwvw#`8SOFWoGUZjxp8!*N;lh~JHpZ(8Amrl z=}Jilt6Ie2dnx`(l_Q6+=9eMrzXQz?{}p~PpwPn|XbEXwQ|R6fG@H=J75d8v zjdqC8yA)dIK=TQ`QK29Bpd%FengbOH{T2GS1C{>jqR_j1(1R3uqXQMoS}1gc1I?lh zn+DrB^mm{lca=iBI8c$WSfK|wP>I=Wg|_fPA5!QhR7S}PiRBZU;0_*W$2Re*#_)(#!IneHeex}f49H?kp zpwPAsw1BiT75WFNRcxF^6#AnB%_Z%4g?{EhMZ)C@UEn~)-g6Z?(+53Sp%EYSFolkH zpjp(`N}-oKQ1R{OK{gKOI#AKJPN66Jpvx6{m;;r*ctxSD9H>Z`q0paExnezEuF!Q3 zR3zM>(B%$PBwVb}SA5XE3Z3DD98tIGH41288*cSsvlJTwmP_lJ zvv^e8VCLf&)C+{#-KG6+m6g?Asqa_nyDfFtPrX#BH?rv9eAut~ImLdeCF$nn_WHux zmb#ChdV*3{S?u9{>~O_SvDg-VY;VP0WwAfr;)}qciapC>m;144ip?e#@u})Bae2y5 zwdN~($g{hZQR>Go0+zh%S%z@^$B%tNvF{NZxAw*T_?L^mu6~|tmFNDXJjYp{KPUM7 zJVkjfwLHFS9(Z48!GUQ5D}ISLpuyAu0i)+a7<_QH>y2MphucC}rQCqSf|061^d@_Qh6F`b9i5voKUc=^6w>g~6*X1J?#GcFMbCrq4ooS8RPZYYw<*C9V zmhyy%?qhlC#`_$~6o(p2Zc?5#mq`$2`+3%XVUIamgNLgMj$GLb%@`<(eHWXynTH^R z-dxg`zdOw@Kgv~>3n3KTPGs+eP)6%b)GApUd}_(M2$}2SXeFxz8OQ7UEyTiBehbeO z3!BWfLe*q1zDCBRo8R&*+kKX8#m%(A6~4C$KgrtQ;ajYH7hAsRe!i6}t?hrH>WJ+p zTC#B<^CZt4kTDQnfQ+`cwp0WCRCg-X%SyFjck%ETzsdqtIhBQu`^n;?DSCk={=Jxf zxzpE4N<6|6feEDX3-D$*A&aPMA+PL>{wyl>L`|MGas)!Kzyl$MA?O1!QnRX%&F1rSU>@SxiBPX}Qzz0d&W@9w zYo*^2Cw;-^R{D3SRU*BT(#NUvj|lZjzuQXxC-5Ef0ce+ zy!1s;v3=8Vanj4I^kw7xk@>UM+J2Nz`kMo6WP(;YuR`F8OzBi=9$^*SBMRhPDKAn0 zf|-In1ZsF;fg^&KEsh6uUd5Feauqbq+Hgjk4Y!C5P3Eb5jUVsQHYceKVM4v}Tl156 zxOcpVPxq1d{cw}t!-LhszK(Q>-3i|xwLY|WAwhPUwAe3B7jKrG?wLSK zYuN#78m<4Gg^wWZ0Kq;6KAsra4j5+UfEpcuM^PsZGq+Qy+$ew{!y5&7bvoCqzF0;i zswTniwd}(wOmH1S%5y3V_CCPbLecs3Cc1X z71RwNbFcKiz`sy<2g1iY@JzEv;VlWj%z<xH$v=+T+)|6!{sDq8Hci z6tsx+2MT>#rB62(De_6yR-VY3{tAgug7*Y)&Q}t*mrnBJ1;*L4y(L{X+~7&G7Ur-| z?Bss7*bxbw+LdUX#qDsur$OXZW3EGO$%{J4IZt2il7St;LN z?~B2~Dy1)_{6klq9_eSZy6N11PG(<~IfF92JwE(v@%E#`=~gUeul1CeZN1Lt?eQPk z$Pc9yxxCI^YUm4lBY|lhNvqwDq)D@qZxqS8A^`YeD|xy|cKdYYhgR|*s5}zKDvZ3p zbDE67yqr8RMAvAzLNwG`bo8-ai?Yq;Ch@gTCy!IO5ny?bEEvAcJh@gTg*36uU2gPH z%PyxDT~{}lpU;HP&cx13Aa-JxOCUMs$043+ome}q3l{n;)=uig+DTooS>^>|BY_Wl z+JO=BPTP-vBMyAq!{e8o&|AOkNKZ;!2ywmw7-GyuOsg|p| zJTWEI*sNAroJfijYDO&aM2Ow>l)(^UoTiBxb# zYV#U#S}sGh-T1aFSLS1sHO^62VR+?*$6^@~?u$1%TrwfAVp3oNj2I(E=wx1A$dz$O zyqG+Uoze!csJc2EVaTy~*JKt+O1BekzD)Wcshr*}zTirFyoR5?+*NQP`XKdM-UW}*V z&rZ(^V|gFW#aBC*@2p>f{QZB#;Yx7lp`nUDL*YJnF}Pz4{@}DYua|^E=JHD#pF?Yj z;}as$H{bCoU#RaSb$DVDZ^sHIiMNA+fVXE18H%rW-Y|jTD&JI;gC526=lwJD!vDfp zk?GaZlzY_DA6{`2%`vEXtCUN(jfZAPbc)%ZjdrvV4{te7tg8 z0bYD>?j|Zc2H(AC*8D3;<}rUT+G0L5PK1YE^ZVkRRDJ0u4bN7?@rtN%cplO*d_VBR zaHMhPX6)Y7BW8CmnvKjsx?OU{NSNOE8L<2tPrG9(ci;AvBk_I}$N?N*m^{*p+6>?@ z;~<$%kr*WZ3&yLdbqD}z8L3#;U1CE5c-Hbx-IVJ8TYHNQfM zzC)o(X6ZGGUu^MQ`#G-UOQb_2uyN7HlC9tzf%DBAiI%lfp<`Jc9o`k3*evhXQ_&0f z@Cc_s=V50%g{=X`{n#jGSn&iS7enkFL1F$=q4BIw0?$6#hLVN)hwnu2W}_Zc+hP(H zMRmn}@+y92124_eJ}wZ^S5b7hKb}lLMwQp_NrefSX+^l@59jlDK>(hCuVN*H2iDFL z!4ZAM)v}UAyVCO4DXF#GuUdNgw0sPX=quEU`IKLK^w`EKGv#+mw9eU3S?eAIA3wN| z#MN6|BZ?DTJ&EGr>gy#y6A`&xTaN?%_GDwVI#Qyg;mzo+sZC`!u7q zt7@G{t!CA)v`-i497h>4%lvhsUAM~w8(X(G0L+P|4|($|{`U@6nQ3@I*YoEbjRk|5 zhUw0_K*b&P48aR_v z^iYcXlmdm>R~8Nu*~!bopIzx`of+!o^^}Emz(?P$H2X^wgVwuh*^8jecbBi()Jt$s z4(3u=xLVdyvV+J7h0A9-*!u-LxsdCFY+)>MP~$Bs-@eX*OL$uZF0=4#!ZT%Q-(9-x zYzvpyUt|c}F8R6;jxN~_!R81w*d@1i7ohoG#!Q2`mTiO@>k!|n;(s}o?i{6mRq145f^4o15iFUVjHs z;lPC!e359t3f{gMFMvNpa~FZfX^xh0#fqb!{;;TNXFf`EgE<811`>=d9(P#)+a$#j z1bL|Io-FCL`?}Zw^OGzC%h!{qB|higKpt4WLJAVsl}Xl2L&=LyOXr%@y~m&TBBLEJ z2i(CGL6iBn$UtyoRQ5e7LbavDQjKm-Q}l#Hk0805%Y(I89cK``ZdwF#`+*&}>wBf{ z)4D#3w(IqEB7GI+H)hX}= zil3hXpR4$3Dezf}pO6BdrubosM_dx$<+JuUIN?uxXEjXn_#~f0a`@a*WlF++uCe{# zj3EOmb_@-bZp;p0@raG zN7Xv{#Iuf0Gg2?`Hene#6Mk(68cdBR|JwU#@GQ?TLuJmFGIw&19suG(jmZTty041q z0;oBGkX50I4K%r{D`w1m6|;|sfsq)dp^DAs-y1Q45RA^TSs;3teHW9wYBxSN^X?ug)KtdCns)HnGm8M z2o!|{B6El$AburD7SDh5X{l+cdzVwc2bl z6;@O(MYS`}LR7SkBDa~b7MV2wheWY6&){Qyi~ zJ7f+AR=$A%|Dfs~)03*o$#63^4v1eyi{f@>(N9*$6&BX6MG;e<;MOgQ z8>6^G3g5%9fbfqt!8i0(*CCQQX12 zqo99T9WCKJojyygI9;qj+d(7W?g5MK`v&O>+xPR1vf*m29PK^0&vY84m`z!qdT(5-?W)$Y^fw42q$oFz3s^11xou*^Jr38OPlKGy@Sfk@JNI z)1H`nWDg1XX9YuvR&jR#A-Rvi>;5NE*~~t!UB5 zcR^L@rIH4z`l&6sa=|88cGXVGcD@d9G_Z|>1f=gk2uQQ`bi~XeU+Fd9lN)XDwT+lBBIXBg9wQ&%R-J@GawLt zoT?H~K@wLZ?bV2zRDwh-z4+VcPbrl)9B!TKPQo-(|TAxu7JF*=jMo_dO zYQJKZK;Gxz9m|h#_uBnOOO;PO1+I4m;@!n6>J<^CFY8BQ)OZs^8jM9}u}a@;392eD zl{82dP}{qEcUkZysR66A7>+uVlfnReBf;}6*cU+^oyiUE6R{0$^nND3&2iWAVA(C2 zk*@XEfw}QYO9=L)a$y#tl=1zW-0QtaW4u+_{U?)LimqE__zH9_$2&8 z_+=K}4RF3twec$a!=iL5Drr*t7m(5G{7Z*{ZKgnzxXZgeeB`&0ya>=ryBhR;Yv6Jq zd6e;iCh#j#;}&%+#EpXDGk=}wqeOu0e{j*(e5ESzQb|MlU#T*bD$7eH4N}z|YGv_O zAZkqWQb~hU3zTZEQt{>=*Dh(0>H(#yRH`a3l{83ool@m1Re_gE8l*Z~sdAJm*GnZ0 zQXQ^TTQ~w}y_ZTFr269!>!Y0%%R1JR+KH+(NcE*sHJ_6I5vpcW@*Wj5SB|Yt5k(bRpO`N*n*$h-NnLP(! z^W#}2A1PA!rIrc~08rj2H4g{8_<|NTcZUp^$n)iqEqz~V*(ku+ms&Q0i6x+$7R(H@ zCQnY5|G1SW%lmgAy&NA8fuL|;hXw!1AG|F&*TLbBnC8y}>(z|IWt6h4xGNWz9mLot zKMLpz91WX+8kekk94YuF6K1l*`*EbB5-gtdoHkdM7r$pxNs1NRIv{OnieLYc3t~HH z70%+yC4OD@q>7F|0&YO{5nGaSU6X?A6^E;Ux(eM@5*7f+nv;|(*L4qT8Nd%#c#e0a z1H-Sh((FQsmqO%7sEX4u+14Luqf*GNtpBv=14ZS~45-sX{#@akqsYVyS@#ozRn$*T zA%rUG#o?&u`%Ys)6@8oi0rPb&h_w^{qFDopo{~Ct$E*7Ih!;aCxOTOx*nEKXy1)&h z4enP8m$h@I-P_pkE+R&^J)V2V5F1$gq#*0eHZ4IVj4ltuB%pQz1K9+x)@gLsYApaf zI@Ph7r-gpLT!+uyZ&|)N)0 zUW>1rHiqCa(BhL98u>(7lT;SE*XDGv?2Mfh?*w542pVw~1CZ@K$OxA86o5xhH= z`pGVgCu>8p+;}p6vCe1phISyci(6M|T=18Ggh41k@PlQWM1o`U^JHHmAeTK@c1^sN z5|RxDRPq`5l(_e~M76|ETKyHC6*sCQsA+O=(&I00F*rcUv&|$Kg}A>`ieGF>?p>Uu z7}NN@!R8_<20_w$ib0bFZ{bhMV(_kD{l&n#;HzhY_p|ZN zGz*x)n8BWs3TIu&LOr`h1!eKeWA-k%NT5T^g({3b(?MOh3d}q+X>A=iUMy{Mp8VL) zgh6EB1PksWk~v)Lh0>PzBc^vZ!H#Sh(;K~$nSnVVaelmKU+YGe`5!YM7OX*`8W28< zSA5?b$M^IUVd}#t%<}2RFxZ@id+dio2Oq`22h_MaGDGP*Cez2Y%JuIr{V)mwAm=Xm zv2NtUjjJtqG=Iv+r8&vbNUvL!ckon{ch;S>Tx|f1>&% z!#x*Y5V%z;XFua~S&FS@Owd|~`VA#$lI zg~pGsup0Y$MR^QUjn69CAzrdwXdLYwtFUPT+W|9NMXk`%m4fUfC7Z2eDeP~pWMy75 zpJ6E&S6*%%KllHZak?_@>t*yrELMeZvDrDGD-WV9ye0D|(@PdJa@&cmV55kB7kU5V z$zAbyBGz8~w4S*=dw=9OI z$8wK92rafe6I53%XM_Zv;mX7HeT=7{^02-p@*JI1Ploc079LFBEXQ(An_Qq<3#}}a*!r^NsESIY*0L-iSb3l(x zMT1~ECM?el`D4jZx-2tVPl5c@;W;WWhi5?pRbZx9V0RJNU@E1`6?N>M*Xgf;7+bhQ z1@F%!2WHA~Kyr?Q3CnTMsp;Dq@E#*^s*C%{d&woGPJh`EX^1Lak{PWNhUg~)ann1w z{xEHT8qi}M+>7Ix=kR<7;^^n59jrD%F`bt%aCJV6hXx!CcY-<(Z4 z0i{dRqZ`0Of>8K1-hXEMI#_;(fEKc915@u}ii<%%-$LJyK}P{vWubFp(2Ee1N;J*n zqc{ULnSZTm2XPa1m&GfhS#m}6cpUMvPgq|ys94JHH2VWCUpg>*xlETybN&^ z_TJYp543+}AofI(@jgLrGcPD|kPh3gK@|n6b>G@C9p# zMf#DuvW{{T;~Y^+gER7p5Ko=cF&n`VT(G^y)PHPYcUoAwUfaD|H10~^cj=aSfMv#o zVeE6_@8_yB?IDal@B6oe-lIadVmgAAxB@-9aj5P`(wLQzRQ_E+PDG)eX#pzcBvW1X zgwkLXj#H`{rKkG&Gk}AgH-i7?gBe94o2Di8`b$iirZ!Nx}H=>srR%# zHD3{P|CY@5N>!x^Af4yb58IQ-m91&kz7kU50)hXe^((=qsDulYwMG@xiU=*t(qJ+^ zKq|Nvl@i2JZBN|ik^IS^6p|N#Sl0a9NbJOSG&$|1l6z6 z@xT0K#SesdlZ}R13EIXlJMTxUxU~HNpKrGhNG2V3^He}<+V8(zoPGEUiq78{CD&8R zE=Ng7scOVay`Ra+XgA@GP^uE8s^qUjB@H^h18${UWeZ^B<6bM zOM~(sBNh5>t5!9RX!mN9yLP`>W!0;#TfDNQL0S1qRijk3UMgvj>e#g`m`-K(V`%2jy~=Rj=+Q4@xhks!^)sLD^ras+4Lj zf1PBI21D2MM>EN=LXk;>^133E24$KccNLVINQIz$C>1PKQ1X=O8BoPPcoaGQ(nqj| zgp7wVcrn9Z4|u%!=IvOJSrnb!BhH1jLD#qbj^VwOL-m|y^?wYyU60V`l&VI@ zl&VCjD*5YliZp21aG^?(si!G2X{ThV{3?|%{p`q>2Ic?$n~nPgQYYn9Q19MEX9B+l zc*bSl*X;~_td|al9$bTv?p4r5@mxQz3p#UmzF?-QgxYylg3)S<)1{^&kznZxMS^=) zbGfiJnMalF6^G58jlQFH%iE2-pR>ALOLAC|G!@%`j0y5D$f8r%VP2M7p#x^#B3sYS z5jDD;m0MtCEnt@-@bkm%HUnxIRjDI_ua_VL|03~FvHLFMOxcB;D}ZOEoBkT3R%c#C z1#}KsPti3EJJB>=iu#Xi$NQP~wvNkbS}0;ioVi#9+z0_CZx>c=#0`PW@0fRwaGbc_ zeGEK~j{^A71xk~dkA^uCCZWP5TnU;%fe6RXC`Wooyv*DTD+Yx73IA@ga+sV=ChJ zP`un?1O8l#&vp1Wh(_c9Ph^0fVDV`V{yoLlD!$O->)E9I=M+Cz@QXT5LqA!P8i%A* zNixL5MIEQQBw3D@0wI|reKDY7V_uJEI`X_SShg1b%5f*o9nJ5civB=vQ$-_FRi%o? z_FrtYQbl96;!{OqzT#6wW18Z<(Fm3y8_`F&8PLsUkNP}}vK zJ6Gm9B2FMx#X1HfLkY9^>r8smUx|k|->$WzZNY#o@6}rb2pOLzkZ&i|1 z(HpLKZ}j4F=~N}Dq_4f~N3twQl@pnEO0q>rkkP@ir~b4Z;AZufpV0wffiA+m+011G zn!ir`q`~+t11Y0iDY=RxGX6SL(je87N+r>StP(GkG)Q%eQb`P{(Mu%_QVmuri6p49 zyj0R4)$vLtZh|V!OC=3b?Wt7x#Dc28OC=3befK{mQ7s<}#4 zUk(rS>P!-@ZB5oc(Uv6fRw-UO1^D=K_@?4(*=fMXm%~RCKUeU! z90tq&+8$Q`3PILr1gbp(S($baxg+gdl~t)-A3w-CD?U{$_Evm;iqwg2wz0?%eA2r7 zzLKO0$1KIC3ddB%=coy}!Lt1|7P$m5O|Ak>h)bpQmG`fWL0;G=0Fvp zbh-d!jc4n4kOg_zSs3w7g<$!HUvWmoe`IxR3ZHM6*7(scHfK^rXm8am@lFzD?U|t!ix8XCs_8st#Na~RUm8p22eBoCmN)y|Lab{>VH0xKwmmw zj>F&aJBe(nK(dse#+eab_7li{i-&y^RssXUrw<6<9NUOAk^}5#fZg4(ECnIY8}`nt z_>1S2OI0i$nTKiiZkb%^wHK9rcd4*QQFk|7Ao#n9k6@o|YMyPE7JbVsz?v638%q86 z$*}KhsFJh7El3M4PtuqW&XAA>v*S#$U{?>ibx=DCDX4@39YNBdpx>w;vy5aIsOr5` z(je7JrJ74DsA{}a(je7KLNzJ*y^ocO%y1);B@vlGIJ~dbG9`Yyw@JGW4^j&%)q*Oo z1=64eomELQ4yUW2<{kdt*swJ3@FJmNFXJ?@0KpBGVM~E{vPFAlfIxROy9#lkYIOa7 zj{BpS|3F6{U{47F`alySSXQBXDVO#rY8^+YJ-seR&f5dAF6N`zxG5tW!Q%nQjs|*} zqQmMGnL+2Wm^q%i#oi?nz1E%uRUc#LLHUab5*INVxe|>CkkL8jy$x2Y?C2jo5lO!u zeQ*9lFh{~pQ%2xt06GJ`FaDN2FYbtnjn&iCh=)+!9Lj@|QtqdeV^{}a_Bf0(LM^D*9 zupewiW5f(*3YgNnMe(=h1eih@xt@6ixrIc!fZ9YlW}FP~{dwILgC5c+TXKXWOYFn< zg{0^ullj+=KAE^7)47FxJuuN1sV`pYhsu*^;atBoJksMxdl;C;8|6xDGBd{e7=vY9 zgwkQYn9LngGWU#QMlca4hq(ip>jaeO%FgxHed|iFtdHuo0sjuz#^Y3Pd=!?(vEuTE zXtk`*krn$3>9Ba#ia6E})L_dxhOBR-U_CF6_1`|$9I{SK!Ma}@>tG-2FF$}aHwEiz zAY(ZhEX(wO}# zGXCOKK#}fH4VIq=VU3?)3y<21`P_Y`#!lm3d~ah=D9`Vl&55}LQ>cuy`M^cHk;`J` zJlMrfSI&5eQxu-Kf*4HehTIz{LJ^oGw)7jXS87K+461YRU6CtddKmIyLN*A@q!kBtntJYRy^j) zKpzIn_Ks(sMCMO35S! z`BW&C*(>{XWok!jhA4RZiu%?Qu;oqQYJWZe; zGlFHu#uNUFgt$dg(jw8Cv%J=zb3_5wUge{~(um`3zpcSCJI8wLZAk^MB@$L?2e~G% z{T76G3smEx&;J!l%oYJ1^E%6rNp39)=K$uqLCnAqzaH2WEQ;^t88Zi z6P@Yy7EUFLBwkMCI|BH|%LU|-4gsDh{Iw1;s(kk2VM`CeIuOWh*16-FCIEL@khq8+eTf1`f8uPC+2cD};1Jd48| zNyC$gK{iF4x)#T|zjEd>U{VaA$pPTEMy{zobjGC~Jq>kSh6_LaE`4V!FTJVCD#MBZxDcv2rpxaF~ zCGCgxUs-p1??T8T(xvLmCsoKctz(H%o!lIIfgn>mnybu-p*mU>-=>O_y13ge#DDjt zbuCf+a>XS2^Aa)0l0b8crwV&&zecB2d|pb$-Bj^CTIdo@+gEv$#^RSR=xb_|z7#x< z$>OteAu;fHzm$p}RoIZ@VZ#wNf=Dl#=R@B{^iv-6 zLq7CnL{EvKgJn1Ag4$Kk8zlB4TFxYt9xTg?r#gUCr|9zBRxa*FU&cDx=gsENK{rjH zPB?I#R;gT_?~@7#%ytbferLpW$Fm@bbtMa-e^pa1c&(DY?i!;Q=`R2LPYiB^dm*ld z#mnxb64WrfP{q?Bt%2k!(aAEJh6m3*J^E8EDBkhz#uLJa)+T`7!lxSm*_yqbAh(_FW2j^n@wiNq?7eLFUep?w{P3AYLGqPX!DkteS2>XSiuEfOl z3q=S9h1mLv{lb;I%YLDZ62fhsm7mytq1=M+6!}T^3zrBY#eO02d|?BQn%K<}829i^ z0?MQK<$~5-y{_@F9s=ei#mEj#dpSoi4-kW1j!GHwh?ciUx}wK|47>VB;NWaJI;rDm z`^Jt)KfEu(xd1c4uK!&tfaIPoSpGqpy{hxD4uPx?Q1ScYR%*%Rv?LPP_n2KNTKx&5 zL3dSA(Tv#UaUqb=FwqDjrpJ=l2}Ko9jn{c+ogQ_?z4dKL7!|swFS|fUoO6q7$<;?e zHmVm9)KNg5fqDitzfu{BoObm!cvkpntn0CR!^3VN$x_u~` z9dU)X0Aj;fMJDIss5IE2-+XL+AMIXSvB6(TxP9+aEG!6vv!{Lr_{Iq^S#R0prJwx! zaee+Oal>Y^2>r??{*kRM5)Foe=-2Rv26%L!9b!9R+EPK1z6OO%$6EUftp5;a>QG7gReX;i ze)9cMY4VjHe0X62rXJG~uB+!wX4Qw#{kf|e-OSe&L`xa&=sgZRSY}VqPs0HEeI(yw5N)}Lu9eY!)Gaf@uv-KlsL=uW%ghgjF75| z_rW1w5WM^r#XZam5lr-GWTzE9OAE8rVU$wF5?5gPB|{tag*aMe|Lg}j?AE>UYc{-3 zpj^mZ*bY}l_i%G_nYk6c>XQ>)h+f5OK=hp`_SNoJK`dV0&YL2B)03K4q;5H1=EYR= zeJHGt9Wet>1P8yw;!Ld=^u@!dsKC9HV+U9(S`2tDYqZYM`H~b(G#eOqPz1{+>exim zL&v8eXyh3)HM?{6d_bdExnZO^Z^+0H@+W~{OtxY8Rx(I$|Jyz*w*dgsyQg%P{s!s? z*>a(WA(*=Bicig}!0p`}9vFAHnCQC--V6;%T!3fqaGnVZsNyQu z0^h51HZ8Xn^rr>x*jq&?EbzWMh{6t|5C}UJpWnf|O7Pe5{18tP%3q{bC47JDHdXf( z(B`DDGg~fWw?B>9k|N(CYw7Qp*6+Yncso9?{rhggvO{@KOzH2tFBA7&F|o5uQYShXLy!RVye?uu>3v3e}WTdEP{}<#?Ez?1e2v+ zNJl21+gDLK1nuo#ygo`0Eh-wJVw1L1=z} zZ;fSMnI?M-xkqH~g%Z2i@Saz{2||W8(;aEqHvb7n+_s_E@KKiY!?sHs(3a;( zx!=G%BhQm|t6*p0`7a@EFe^N`OvSw{xF++42e)G?eBGawiYgxSV7@2j4@@_eX6RB( z=X@-bX`4>J#=&0+3<_!kRuWWwKE!hZsbXI2-qEIVVF-8Kl>p*SdGA9Q-Hn?GUm+M$ zTB($DrYM#~#phDy7E0iRQ+ytqe2@dtDjrNh6U>h?!gQ*KzACT2VJdj8)Y!x9_fGo? zurB&|D6EjK3c&_dA%`jCVJazL&lO_&D87n8=zBY-q9)S|zoGKA_`Gen1!fNY7*~HS z=Pr}&?n3F-_m^1z@hq~h;z%8S%~b4<*(r;n#PqM@en?nn>8t6%z2KS-!oCyGDHBRzKu19|UjbutPyLTSaA? z?+yH=qK^Xv%jd8Uz{>&GoA4Jt_aR_8r{NWF{4O#mMF1F z#fRu*6YAdlm95xXCK0%^EWVyia;GbPuHto(P~+guV(W0OT9E^_?rUIk9UIv%ztsaJHiZATxQjUAmGN=z> zDxj$FWw4NKy4_YRt25U? zJH=m4D-y&h7c)F^N`5jBpFT;l=N3+ib!qHiB{BHE2@S+s!jf;sGy3v z+KLjxdR13CzPVO4yd%DeRlTZ99K37Na^M-GvrFy6_@ zuLN7;#Q-ArtEF3dRi~rkNFSC+RIjs|^FhQ{k6lPDuUqMbphS#H_@j==-;{icvRRS> z4~hIulKn`6FpgpQ4wp-o5dO#8n4Pe&tyY~yxG);St|Jy}U>C~{c6r|c7Pxw=c8f=? z{7vzXS(3Syq{c%cf0N`ElEAXhrHWRkF>1Z4vp^awv$vI%XAnl(fuL<{qLgs8$m~^} zjh5t{D=^#YtRz{l>I?^8#WsbcTYM(kX4n*5Pk$JH@nvk;p&r%sxEIB~=nx;21M%8w z-Vcc>2sjZXFG0){+&jR%Jpt1OhtsQQ@XjL?F_|wKg5^)jjDlGbmv9j{PK6&&z!VUf zJleEaAb;eWw+I0)!)%#vr-xaDARsj?UyxTQ@^9wA!_Qd04;O>^}fV8PrURD)~5 z>Q2k`j`aJKWiA0QSkFvr9Q=)nuT=bKJu`LigNTnT>NuZK#ciXD-(=wIcE=Qr$JxRd zHQmvFAH8$+w*`OWM3C@ovk*dXrxA?4789M_`1Le%Z>R6x6TZUK``5(_VGjfxgbK?9 zXp3KrZ2jPlduk?Ch?#UB=t0V<3-5kRw}nAT|Xe!!D%qJx)BZNPjacs!5)rA>qp9z6MH zEB{L1$={23dEX_@XYlnmpc%uC0LP#=D|8<~iM?5e`d?yfgE?SOci_zM;PE!%ic z&wj}D2wfHAr&B9wcN`k)zQmpd=Kkpk)`}IX4 z%6-YD{rN^KcZAYTOrX8PrCqMHU4$0)zcHz!Wc{H4(4XhDy^ppZzRlwQ(zx8I?ageX zue2}va7G9FDuB_4o|!ISjs%YVxRAJax?^@QwTNO~_)65?HpSoZ(05t5Z$@5tOjam7 zI6YLlI0J<>J1^2dv!XvTtv(NL3al6ivY~j)Vlo!_c!yVUYplDc{n|0job)>7AJ=g} z>7T)y2jUa3U038)EH{h9VwP+iZnVUQY5U_ltv}^eY%o`VK|tZ5z&7E~w4k0w^v`;) zB`(cG_V4=90cmCt%!bI_iaVWwGShKt|Lo|8cAMSAm-+rV&w3otp}7v^c&w^;M)Vmw zjv~4T445N0!%M6HlVh)d*+=qbn>l5r9gt5jfS}0<#rS@DE0OOjz*r^zT4DJ{318s+ zLy=1;SXGFwO%c3tT}pHDZMdm#+TmT%%iCOx*(u|nSF-Z39>UwW^hNFAz?UFpP$bXn z!%^lHC}9u&rjF$Bv@c-J5nGzfi8@x+c+ne7XP~2XrXzmj`hlDqo&~!p>SdwT6Hb}5 zb}%QfNM5g*caKm6iVv}#ZE-y->&T}J>0AzsSjGVv4!n^?GUlTiP8yRbZZhcMA$&KO zUK^2H8Xs&~MKR&P4`9Xo;4o%jm15qZa?F!^`ppcM4acg(41&%^uCn>{NWRS)?{}ve z7y#@a+JSCvDn3-M8~6r;Gk?4cI@1bF(Y0~IW$Sz!?0YB)65D-lBQ^=`*gW+qsE8c} zC?~hgwRRG`P}|#H7Uj@JD2mkS=b?E^=c)fxr0W6O*NxVN*=qMNZ7ZlD{Xub>O z<&>ut+EbzS)FWW-R`?Nd@S7CAR~&q}!v6>RZBOt#BVf){_?kHQu?l||aAx*7pV>Zo zmHtbP$YMWWc2mMfg%J0nC}D*XqDnKIYu>N{x=F~kncqLP@~+VKPujjf+0YBDgEN(_ zmy#_}vd&6|(M+;QO4iN~{+ELPq>V6Isn1jJh8UPr!l?@W%n!~|u=EdvXw_(?;HN;1 zwMK)Vne_|t0fHp^iVVg$UbA)tlr(&N3mP6#2k8OwEtL+1|eLLC| zz9-rY(G6&ub6z6TxNLJHq#=KH8iT@3vV0|9jW)@1*^VqML2yh@91$Qcg?ta_V4erC zCxhir1ILddq|bcwW+4m4l>N2eXs<(vU{C1WNC)A&aQ|FB`oeT|khnC|!;Sg|n_ znb_T|!0`iuY4aoxd2+B2#qz|Xq!3D&Im`Qsbo{DtmX;r0wZbPNR(au5%}Qx+YwV8# zRB8IpAr^Tu7P;W$Nj?1KDq$8#8IN3s8OwvYw3r+A;*;2{WYcpwHekk+0Wr}_t^qSj zCHw{B&BbU$-;*_ZuIzII<}Ag33%qyJcWEZ-apGRE%K>(Ep?kqu5)%0~^q;R-Qv)~* zVkjd(p-;D&^+2$~ZDfq_TLc7ucYI*`_A0i~XC?{&5rfI;wPnSDNo|?Azt5HqVDoZ4 zmYnN4Ki3BM@8uemoaghEA_&hZXZ(`IJwDWm-Vx0C@ zlXI2&xpuIFkQ(_ss}7if=qE<|M}TEzH}-woA75xkj@0&2w9S(*fV#(V9gA}9!n?by zwqzn|NkOG8J7z_{H4{LF6Y)H3&_LJ=^N`L~b?p4oC0Wt+rVRb$rF&KR_ELg!NoXqSY{)~dt2Gvt>innejoKcf?QgW*Pum}BoBiXpY95av z6602$;fvc}$WhKccCDC?qC=aE586|qD*(mSMbyUX?mUAJm@Hy&i3u(9;N^H%NNcBc zCtCQshQ+d1b;y$ccQ(b>nFt1(oSByMO=82<>1dhGRCF{}JveOX zUu6sRBPzLz7;foW_)N$GYw@b+8c6YoE?yRW-&wsZF?S$@&gIk0VEGFe9nAHf5S|yt zhj0dzZpvl=2H4eBuzV&c2KM-~cmuxmM9*;>hh1^)Y3gN7-+4hS zzp#MS+@^+RUgyvE+f1Kj5W zA`>!jNHioPqvZFDn{SV_`w|xhc6O>2o-;}p?-u>Y9D>SYZ*MPMh9j;&bBm|pt}reZ zkq@xV=EJsGOre)*VJF|wb0M<~S(G^-d`Dq5?-y>&%DvGrF2*T{XhivZJgC zLszcoSCNM@Q1NRXj85O#Ze>3l^Wc094ZVxC{C$a1bq#(zu{M%10!6G>1IhpJCk^6% z+&c5r<$NfF;nN60k76HmnCV4W1riTkDJ#oxHFqN(@fB7)p-d3`@IU-XIsA{}^NWuN zMJDH)Igevzh)m8jGx-P7bi^h0o`Z3Sr1|uD_`ZtZrTB~U;z)qmo~P|RZ9k*!InPSq zOgF$afE7TI$OC&^f2Oy|`Tpsu3+n)V{i4JQXkl6ZF*U2F8tNut~q8h9CxncryB{?+I_9ocj#3>s(sd6%+bZ!3 zAeOaF7Lhqt(pGea9EnLZH?RepZd182%C+W0cH0*m>xDF?#5Lv>Wg4JN`4nGZ#XqFD z0ZcNH;Q1vu~*6@I3|vpD*=g1%1?ovb9g z5w^zMOjKefIy}aaoJM;q^1OS+vj`2bL%vl8V8eH<^T0d0`KjmW1Kf z>%_0=1dMr_)>sKl@hUgPKNU~ zc^+!^ehBbn%iffC0A)mOHd5Wljs>| zleZuK=5}qZj?r=CU{q=t4c{y@pCrIYoPdc5W)&`3mYPkt>h4Z7y?GVPE{ zB<#sl&QvZ@iJ(|#SSKn}N1W=L6r9g2VUfFB0Wzs)0FbXDC5mcDZ^DpfU#N$*j2YC` zOYEqwy>cZQlBT@Vyu5b)f7147BG1RfK;Kp1;q2+ogk-p9B^J06<&(0k@4MmGh9w?%C&)6b+-wrg*Fx?4z z(7U_3FpzY2bxaEv+&E5_*D=un^WP_|)9OO7>~xTrwSc-GfpSqt5>-PKK48pofy+(e z!5j)fT>44W9q7XfJ;4VpQD~+QdaXit_d$m!bfW~!k$bvAzw$w|61&xY>dcK0XYwAQe|2UlfaV5tle1^}LClRQ zE1GSpK*Q^@x~P)Ox<`aXxq$GSF^&kpvfKo^Tp?+MAf+X3FnK&`&Zgk|O)h>KkJo;z zg89xm?-+8T6W(DUyn2e4Hu6qUt+DWr!|J!kEcJattw%mY)GK0wB2dXBY2R0P0pObzZs{w%csiG@X*9{<%7 z1P>-ah#CA48+GO*0L{(xErRfw9jh=xK{Et2j_0fCF<|VU5SCA?l_p2pO=c=Ya9Ndw zHh*WmX6KN(^AADtO&n&n&14qhbp6w52g)A*S8~cvG5>qEv*|kGYY1!L$We9d6 z0G2NHFx8s<0F8ctp7$P1{52h>>D87#oPrTavi(+uUVLaw-b1pG&(vK<}-SAdY zMC25aJ-Nghnavigud#7b=)RVQT%#>ln$k_P5k&6N%V7D(%qYm$>z|KGjB1F*&Aj-5 z!esm`QO1lv$o3$vRKaAr0kh)iEgT0>Ir1|XbF-Ob9hPYR|BlQEqE zpxW4Y5d)BrEdg^#GRAZd$f~C1w@nxr~RN zN#;h{$t0C&J263AL87>&=3D@y)rr1-Wx5iB8$RQ^c#%1f7`&@44ihXppLy~dW^j9i zgPFkX*&hJ)4()*XoDlTvH8!SxbA$2}vU!QJLK0JFG)}jIJfn503d#~e4&6-&bOlPc zUE<&_k_HgWJ8sj8_T_JGTCse|FW21uw4F$og8?%HmaMYU;z?kxO!elH_6K2T+x4oc ze>hg`&ES`8#R#ji?xyX_kS3clOno(G1qwiP0hw(fZo(@sAs)wRgvrVX1(oM6vsfws zD^%@VL_DZ4*CN156qciOT=b4r*cPd3wm=UfEbbAbsJ@yGOB6Hur32FR1+Vao!ngqD zugpjXksame6$x-Mifs>CihDs}uKhQy$NtyU18nSgO~$pDTyRrdg2pOW1ZPQ)jZTQ)V&#sS}S0NDq- zKf%3Bbp~gcUe&c0IhjH+r|d7H?6Vtg9Ru?vqO>~4GQDq^LKawsmML0?@2bzTcYg5x z+T%nZ&Sx1?ooA<=4SRL+bFcQaP|Y(y=vb`s&3)G5{Ku16{1K3}_)4q5UwL7`Y~sU? z#jCBw4_lyE{O&h?i*FTyIRA_Zv?+8s5bTlrz(9@v6}Kg{tRm3WM=73vObn$hmi1_n zS!dn`ODr}Ss%e%8iVyaMW(!7O^nC5Txz_S;0OP>xXH{tL&9caMfTU#?S%K0^5nJM9 z=6RBMT)P3a`DiWNuJ+ssJw=%D{_~hyAuD#Y=xi{xGraL1W|8Nr&L8NE#@Va0v(>qU zI)i1L;H&fb_qo)Mw_Swe{SKJz9F}hy@l{;1e?7>&z+E*tleQ#M}maw9<^{PlX0v{k#$|_WnoH+ov_&Y*#7% z0NeM&nw}tR9FU9vRviK)T*#;Gx(4C;@N|%y-N@Jte=$P_^x&P0n=yrOtllX>Tx9pj znA-E*sr_TA4hgT&2k)zjNj!m>CQ^5rti2CMX$o|D9k)GWBGH>>aV5g+u`WwMnWpu) z191&%ebHV6a4v^APM)W&GrveQwwcM#u_ilD17kP(yy*$)_H;8tOP}3o^6ckO(Y(!^ z1-2+2OtT)@F|xbbOD?+(-x6W4Ji-HJor36;ovS}mh#U>cY`lOF`_Z&YR&AC-{z|Sq zCdek!&2~gw2H^fj5m!AQ`C?|RMvAuAG|)@SBY|fcES)A}K{5ajx9Rv7;dN#eT=9&o zY}Huh5SG%Jc7G0P0M>m7H!R53|S_Kr*FTr~@&zMGMSrKt^9RA(sLMSupK+(PV4Q{OTBBT{mk>(5`Suj$rMn&T?NQO=Y{UMOLFXa6 zQ^AKSIL8ja8(eNUk!772p`<^n@j|-B^aD7$(tGB4#g?S;J!v!JTc?pXb3{zsMhMBV zwSorPlMZ)$)e(rq9xqJZUG6eVUcgLo4^E0d<&i~<>|65$$Z)+le&Ypw{NxDWRd!;Y zYimoe>=q{BXyEj1@-boenL!kW2Qp)@fH}>I@!J9MM_ITGa#Tl}DZ{0Wh5M(WVEMiv zXyiZ1O#OOlGE>XH#lULhKj2A&{pPzWre>WPAibCv;`@@yDMKdhG{wL*P7-0Rd%8Cm zPLzx`dTy_O0K`R!^iM1v!SZux6aPsP+C! zqOXb3Lo%0qsgjSh0WEM&xy{0>EHNi{^xZWodN+vXD!<}M z8`uBBcG}^QJlpgJvFD5#wP%Z4d$L4&vMW}5C`RfOcx*9a#oWk&jT%@&DZofYlhj!u z82=3Ow!(5b>tG6?gck|JkUvw*)w6G(T~k#@jq0f7FW#%MG>T`Yk}+JAoB_&CQ%seL ziaBzmVoDSfbEK7E{EqxmPPaK##vJ)hVOiV(r3cH8;2_~YNsJ7Zbq2z`43@^jQ<1PR znlMTJk_=36HCXl|lXNsm)}|tPmn8j3GCvgwPN(tqGm>CKncQ5CHM6q}BsZlZxtJv1 zkmUSSB&U#M0ZEQaMY11B9wtevR3uxF(PkV;HZZpnQy^IO2}$xvvLqGBizGRoBr=?m zNx6$8dy!-^{zBbKcC7JJsT~1{#~QVYDb#gl34bN$-%yOCpB$7wqY%k$WPd%=P-yQa z#Rp6R+m|}7ilj~vvipfw+Y{?v*VlCLB<0j4J z`CKuDgm6e;{8bZ%7hdkvzO7Yx^)A;8p}_OD2s_t-3#W4h&tt30C)wvtCz!Lj(TiXa zVNSzejjyH@cAlD0scNcRHQl_%A>Jh}roCeFm8ZaBg5_sHO(XwF9`NsRSVc^nu3-5- z|O1p;A+CiGb zF_j$Oh@s@T1sv4+ojR5n^J8c_DBkf$0K$t&Ipxfj9drWhQoRRhU^J>DnJVAU0wh+7W%;BX5Q z*u9n5e7xXG7tTj^r4=0zDqT2>fAO(e`O7T22hNo)tRhtKoVP8Wf0@TDo;UNQ;jJB| z6DOnFmUKZSx6^1S;aBEl#uA8WDYJyCx3* zl;S^7{D3(89g2TJ@txxEmn*(P@vY+UXDI#}#Wz0fkH+DOAE5XT;_$mEzLVl#h{J!k zGhK3|mEtSn@QW4Scsu<^dZx8yoF-?t2LpdIZnJ4UOWSGMo~G?B zlP!L#w(GS$LEE+39pzT_1kJfgLwufnZuC_zk zuF`gQZC7eLTiYet&eV3Hw$ssO{#WI&`|2j`)wl0FB42WjQ#y8t|l`mL6yyXDCio^7TbcsPIVxaLTX)pbw$ zY&}5roeo}7Jk%$d&14D~b;-Z9N9DaOP)RUeozmE)8TssyjCmXXj?I zwxz|{dATht!n`cCA=@_Lg96@dF$`iybhuP&lg$^5BaG+RjOaPG)VBF zQZ-Dl8ZP$p-mkoGCFLEWyuB^&A%5NqmG?HyimxVE{~z;(I7g|5CDri1GWCCy_219C z^*8lDDeoJ~`-J7a-OoE;dFv4~_F;Ex-Nk;Y`<3b~p~9U46VuH;pk#j@Jz7#SE+=xK z6;VclaV#OhWOg`~&`98q8T8#9f8A92NRe*G(A@FTOL6=^l?wkt;g9wIcJ-hAo}lv$ z%X63T=nFJjm>T6V^fzG^n__tegU2(A-2)zaJTEEFd6wreKhH4bDY4O#`QeM3p;MN6 zcejE+d>-@;{!zNn4tYilLtf^I?Nf;0fF~3KUS^2zEi_@ce`w(e!~Ipj*}J!nlK4%W z?#sP-+id*4LCl>wEC)8qogi4V;`{-8JUL&J&fjfj1%A7!`Iew8*ZQ6TAO9#ur7_kg<{ndNVNgSyiNzeeJ7CaSkb9@``00B=dS|`(14x zD-v+|$6kh+sBHTI%x;^3rFL{PtiXFhWotZK38pCjPxK0XkZaMsh{hYWaW^rJbj)KX z*~`~Ug=!>TGt+A%?j}A4x6`HL!QM=3>mFn(U6LLB&P==x!8jTn*!ZLAfVB1QYU;*W zSH`QbYzoUM>g^Y{fK(!k9f9lRBRhtQj`PzzWNC7u-%$f*2)yVIzh*Mv(Z>iw{y!*g znQI7}D*aFuu>CNXzld@tlSkwc+?N(Au0(MGlLH*b`Y`SvARZwp>saD;P+OqM$j6?z zf9LHcf8z9qXSNSX6cdHcHETf;eSv)SEv+$I_zPnmH17}>Nn%W}oR9N0j^}o>{a72s z`$23@lg>}j6D;dT(i=e1*b{$IBz@y&rzJ z^*+ng_OpB2`QrE_yyqUL$LmciTD^_|%7^^ors(H#kHyoV<~;HHxj2?UB2E!`JI&oH zb^qzPXY2cT3*0&h*9$fTtot2#HKyoR|ewbV; z%_iglN8z*DX_Mmw{rQ#4;g$dC7e4i8gNq^jY%1f@U{2Ncy|`}2_AzKn-@U8^2gIp) zIJugu{c7rO^m|#5oU5y!>qT(U#dpV8Bd5nnX`ft5bfwS5tKzuU$3p21%biBk-jtPGtR+RbE&z-%#e4{jJP(@rXr=SSpCXrW((x70>>e#}xmJ#kYAm z4nIlpWyC{sYuP8@`G2(8n>)A~2K*|;Z=`HQuH?do`GtLsV0ld#l$`4bKi3R!(Xk`du`t?l=DDu|ZWRLDzozYLwf#S= zouiw*3hS8|i9Wybna}5A!SCgIF*(;nKi5`-#>-WdoU6Z|>+LwM)01-r{ajPE8#3S_ zLo8c_mAw)BTE|Zx_$LV-SA{4ntAym3IHO)+=821o`(mp0#D5Ye+&*$Fj^*XO}I`e+# z_xpVR`kDuw*IIk+wbxpE?X|D3{c`Z{2;OJoF~Z~P3Qr5Zor5>07{>!o_9xkRZ0+;K zj$47x#fT`Qap4L2rX*kSY7|XYR|>125c!z(@~YFdH-5oL2uU=Da&;caaF3{iNmMQB z=6Uc|qt4^P^XE##^Py;-pMp&Dw0q@9|NMXTq%BDc+#({?LTWuWnK0h8x}899PIE6{ z-r9=uvV6j=t=rX&LbFcOY*$wcyja6JZX~H}B7=A%ANNzLneLRc?Ub6WY~RfRlIv*D z5Z*e}SbR8Pj?VP3lE-Qm4>J^33*{StnX|K|d@#qC^(UyFAQi{p`X0Rrng;J5YZ~Y+ z?O#|m0$SG|;sVdR_7#@9XcrSMh|Ur8#%OdaL9dKPA1~-PqS1$LG2%{%MsF4LXhFjP zSu-1era5i~+DK?luFMyFPlt<~V6opOW?LxS$ZHoF?d(K!65G}5<4ol?LtZr;jO+_- zL6u?VD}99hSdcQyPtb*&q*+|bWXyr`Ega`0{=_kcLlyo;1>lxU*N|9vqnz_Oz(Y2;9~^72Qa3j zsW7Lx_6}Y9qV+~Bz>TA?~b?APh+D(xp z1r%2fwn&l{N!lPu6_V68lJu@5y&*}*N0OeBr2k0L{u{L#6l1g`6-d%1lca`d{C2fo z;9deR6}YRwa|FIv;OPRV2|P*Q<^qp4FpEymsQv1XQN{u{M3R02iWc}zlDY|(4>f+f zx>Xgv(rkI|w{e;BXP@}==SAqlgSgCy!djy+bE`Ih(+PBulx76fIurr*wL(*{b?!dHUU)H9WorYtWPhV=u%GMXfCaQe{4YLrK$;eyA zsNlE}iHx2ekz<&yIp}w-!S@5~uC0v`GrQ5RV=V$|i~p#zkc(X3C@@w!K8DNV@wwa- z&1Dm{1(zYhWxZG>O=Mmwa95<0ZjPqkuNsKL(`|Z5pCoW|^3(Rw#{OKY>JBDH(93}~ zw&#_(b2Sm-jV_~BRgaYV*O`vIY3EEe7CHFF$B}7?>PDn7MeoJvosfKD;2#RIyA3;R zrPKV}=&ThEx@H0Aia8?BteE`i@$>(IZ?Z{c>Dxlwy!Wvdjtm5qt}Ob{xpObb6Yv$v z-X^B(Iq?~di7?VO;F&#aS@1rze}NNX%Lt$(w#1z51I@Wo8tca`_T}+Q^VU_`Q)(01 zVog+RIT$gkoAn3F9E(t5k4VgSf2V;5Brc0cJd>%2yL0)gch3sg=HXbW>b3=3f7q<* z>H5c-hMIV{@x^p?CDLj(x{DniUz+SXr16&U-WSynrG;;0%tDbF>O2;J$~gnSHB;T} zo5pAV-BKtUY07%qO1 z;tN~Evb!D2o(vkQnZq$m)A=wim~c{H_74yBHI6w2CUFaULVRJiE{PJRT7=1gFza_4dRg&TcbG%H6dIoAGVAF$%Fc6Xj%Bh}(YmoIh<6n$8eY%pTqbF2X}p2F!7ZT1SbBk^?g2QbT5hOKK;Ge>TbeRxcbYmj$7=s__FL< zu+9r{-}=J@yXM zi`|P~dQgh$^e!BxX-olq;}?(~U4ZQ{mbYXU9cE82)bewzGL+ODCDcrJi&rl`(@z&V z779NW<82>BYSPVVrYj3N{iu4F9zV++t}LyQB`R(Vi=QVel83e$Y})&=(2@$i#L7XH z7BLdwWwpX>HTW*gkuSciR$pBM^W(;iBhzN7WlVd4>@&Eimt%tYYrqmG+ZxjVa_mvY zvvnvsT^Ajlr64-X&5|V~S@uEd$g7;)bt>L}0v4k+j0jOS3ghcHt;q<5lsjp90MlI1 zbEv*EhWzVlL;NJL?iy+t@f0_D`vTN;Be)TRohD53Z&^>ob?~3YMajkf-}^m0>3Qq9D$z@c)GwP z0#6c{^~3z51s)>saDjUXJW$}S0{aENSl~_qrwQCf;N}9KDsTf0WB*Oyy;KbKW|pD5 z{o9!4J776u_-TXD^1~>qRH0fZRAoXnD~jr1zTx$_P(3454@FUZCRD?Psz9g)Mp3;b zRNaK?N};+WifW=zr3zKLP$frE-6&KCp#^oUCsaREE5{O@gz7V)+O*zSVs#W%OQCv8 zs1^v-b5T^guQHaHC{&Y$YLvi50uPJIwODdpEmYSERp%(GCxohvP+crkr$$lTDO3%G z>NuhLo$@<6TSB!(wCVtu@ugbebb-qRP7!#n=JMcRb>7=BQt-hH_18Q4A%&r_p%rW- zU$1@`gi#T;;e02^yW_!^??%Ogd}L!A*~k-nsIeS@*9e>;@Irx81)e2vvcS^>_6R&t zU|l~9wPqhO_exV;UupIBs~d!DEnvNCmM@5fK)A*3FfXn)jia@BNVCQJV#4Jfz$Vb^ zfLh+uls-Vne!STz-9z9{1@0(txxlRjeox?21b#{2V+Ecf@F8f)$y>3&HGp-@H*4CR zM2yd940KUj(4Co`fwP!`$e6(s1F4ieRejhGH!$EST{SwaJ#67Qfm=4~2rk;7X*NWgiz`l6d z3*K>-z~>8mye<~sQ$<#Kb3LX9Z^qftLVY7d8gGQaXY$^7HkJG=-jbeGU3iZA;~9-m z?U0d-zXHtPGc}`KY7q=kTV1nta$Y;VQQBDOUI3m2{W0rwz3j3Fzswf^Uax`Zi$}Sn z@?{T-;$Bk8iaNacKGGvgD*0x8=0zgut>#_173wh{Y8DbDi!Mc^K!GV*ko_uSKLiLR z`kswClhIG9-;H3qwL<$z7>tx(^Q zv|ly4iH!7u4HheSY0?a`Krtii1DBHok2h|d zWRXd3hcrnRjGf~Hx2&B^${6xR%SJJNB& z@>R_;YTWpnMP+xXAUQGT<54+3ljPydiPVCq)CEXoo-3u)K1e0it5(iQ*BmRT8?Gpk% z-~VID^Z+wDP8UJSK&bu1(0!}CNT~V3g<7ZnL@m^myHM||Fi~?+g7G-xWS;?PsC4gN zK@hSGViEY=HpuAKQx_S@xL!R=ZWX#kO@7gYt%>-hAy_};iQu3ecZ4ua61;R1{RO=T z((7?e%68b@93$7SK&7iogychz;QJ%yEa90fc@!Od|GpNUnhku~@Fkwgt&jmzh`YC4>kPsDN#?<#RP?R_1bke$I#Z6}}=<8`Xn zfiBE{$Z2{Sa$cc<{p#(30NMkftyAr#G;hfnK&czMVmYFPQ1%vqdg@=;8@iEQsIrA8 zyc_*Hf#l|3_agx99rCX7DPTht-$M@Iqheld7nAlB)10pao&@vjcrK~;BbD07Y<``f ze;r_|{8}cXfK|E7wq5-TusSE3z?gL9*UWH#5vWPk2C0;Fp1`LH{IbAJ1b#x`KOsKp zO9bAbVVuVlc$26%1TafqFK{n`mkQif;5h4fhP&vT;S0f#{Qqc!v)^kU-TDvtH6GNKNGlAZ&8vYxBN9uftAFA;MMwT0hVW2qB)*$l92~l%n zUt^mb!gx;~=juUb-rUWbtWMIpY*uG#jwoUl=qX9E&X4_XfjxjJ!bIV>Th{^ZBNup! zSZkQjS4$oBlk_r4UwFu&E!)rN<%ooZA~; zstp`ofzAbe=8j~|C!GNNDydzW>XFmTQ&Luts!zoZv067chpFDuCv}F^$?eMU$=TdN z<^Is{v&C5=2_fcr)M_2&wRiBJGAD-cNyII zg46v+C^INv4SI^jTDnl?z!m2}t}1@${bl3~;DqmX^>dc-f_J}9_JsSeXPgF%yt)@2 zjvjgF&09`kvtEd+Ddp_r+Tx`nH5UDC{9lqSrcOIyWz6U!PeOGO<^_>Nbt?wlCk8XW?;}bDSd`}~=Ew^uO zAAcybJqpV0?GN2il-mn+bHXbbo@=bopt-31QLft0TZ779PT7LlSG~X%`U9j~M$IPD zgF~5LjY&gYXFo#Jw#!iO!R+51!UAHi2jPk&sH=Kkf~T+G9jH4r`E$oyh^Gc=iuVj@ zycN*b>vQWKZ5p3Q!?)C`)T5ehVUZQ=J<$sFoM{Cs1B47L2A|jnu2SZc(F=eHg_o zg`P$&V0f{bdJlqIK~ar51(#6=sHQe{@Ob|1)~PR)sn54HLYdEv)@szwC)?WjWCevN zfqLQ5x)i$aVo|80o_@JB8H5z0$$Na-;b_uJG}(l!AT`?7WC>J@)+7vZ>(JylBwZb= zNfuwS2xY!l5Kj|IpuP;%rOBHY)uqW4q!>-YeEQ4LJnN(MuoK&YKwe4(8dQ+$NjwTm~CS(7A zCPdqs41#LWbrOWQb!aj-Gma+Te5f^>4Bw}~b{?}sKt+nTI}YSEhPhq!fUa%wzHx^K{R(l)LpEJ|$))TPOi3+vM41*8~F z_VejVN0TJcB$b*FZEI2sM6@Pt>(=Dzi#(p(Yhs)HXucN_${c4p@0c*mvee0SsWfBDMpnxRHcuj$}9I9_v-ySR3X|{<>Dh$xe@Z#Q7I2&*p+)k9Dau{YlJeNxhtN3 zu`G4f#JXzboH|uG)=}jKJ`HwM=`5-|vJa{dZL4xCbc?Q)ryy?~sw`~>RgUN!f8rYu zq0EM63>8x=EK8LwympHa*$$bn z33aP-E>!6o+f@F|*E&L(6XW)dEK2PvtxJ=N)^%wz8!1MU1ZwiIqshsl$@$cTXj_xU zM`&^>#I2)J23{C5p5)l;VZKQd%KU6(ygFe~>YO??IdpzqntYEGqsgVzX^NxC!uyPS z4gM9H5N&I61yqZ!lRF@89hy9TL7Y+KGQLw2$~-TwCM-$~D5&TX@)g8&YvK>imytaeLk_iJS08G65#;X~*xhbz zZr^sFK#_Ouj;9DqQfnTnOOfZ&>r$jdr{t}1X6%JtD|GY50i3JnUYC3QpmKFL>Wb4w zU!XF4^QHC_vOIi`JQF|Bg`eue&vxM#y6_*m@ZY%bdtCUxT=?eB)U`Z(raZIUOI`S` zF8sAF{4Fm07#IE#7k;J-|E3FH?!s5Q@ISfmzq#;@oh60xa7%fnoNZkAEEoPN7k;1% ze~$}4&V`@q!oP^;q4t7>p4S)eNU2JE71JiHFn9u^({LLo{!v?|oZwNj+Z+#HtWp>AN7he27~a3Dw$^(_5i)C7&yi1=n&H9XK-(UC6$Fco#XFMJM|vcm zJ@_#*-4E)scHi{N6mU4Nb=@OkfEP1GRSD6*S9Y>jqC~7GC>R0O_3qR`f zgB3`eLu6IrTH#!oxQF8tH7d~_FHV6y^1#Be2Xy$BMPRf(@j zVrAkiK7|iqlob)cDSrm3QU0H-z+iUhKbrr!;E!rimB>_-J&p_t>J2Ji(F?^@Cf)@& zL0!f$Us^UcWiggwa4mlIVy-lJuUZvI>U+XBQZTb=6l(FNN&m^D-}f8RJ3HyL*Jn(DynZ9^w~uN;X|H>bHQWNkPI;t%Md(j8 z^lwJ!JClAJ25b(!_V?C6y9(0&z8SRuM3C9KPDy1 zq}=#p13E@)LREe)ofV z-(d3%_7B0nV6Z0}>~De%8tm5BG>2aV%kd@nK4-Ao1$({04l~&A1?w}|_6GZnU@tP* zO|NRcp9}UhgPmZo9|^XR!DbljTEXt&m=W_CENH@1<%0djV22p&GQsNcWhnE}mo@GC zf_>Z2b}-m?1pBnX-l(tp&7LjTVuNjBu&)UA4ukE(rS?$v^MdVbupWbbTClkW`}0gq zJ4LXq4fY9x4GFe|!7hJZ(>^TNS~k^C<}iaDFW9ib-t(NMeNeEU8tkbCTPWCN23v{i zEg|n+f_=?kCmZaof_=uY*T~%9VeN<;R?a-=XfrZx#ek%%@XYQ2K$J?UL@EL47QiSwiWDr zgFW3~&lBwP2D@gO=6j}K0|tAK!JZ=6kp_FJ!L|_WAcGzHl;+?SY8DctU?-1-hjE+$AV-5Be!EQF#HIp>$4T4>3u#X$;0KvXzu$>IH zw_s-&?1Z4^a22pJ8ss>r2#I~;l?)&0|n20Peb!@#v zgV}>4^332^GL$)VzSe)&p{81%hYdE_V8epF+hD(*r)hr_>;Qx9YOvo5HqT&pzNcxw z6zqitJIY``7VJp|ThCxW5Nv&eo%F8eP$Ae~(zLuS4R(cKzckqDcQoxH!LBseXAJf| z!Ok|=UIzQ7V5b`F=?43nV8`TwX9atW!FDp(se;Wm*a>fI4*wDC zc?O$dumQm~GuQ{-(zL~bJ#@C#IL%~CjjJ4`pDwM@Z&Yp_EN_Cmo{8Emq_wi4_+2K$T|mz^!x zXAQQi!Ja1AQiI)TMqnoj_AY~6XvSL21l!+WZ#39r1>417+Zb%3VA~q(7iI)jd(gDE z6AiY=VE+(og2ApcBe35DTXUwiLxI8mBG}Ij_5y?5F4z?Yt4y%?Ua+qlYjR<8#FbjC<8qQAU5hJ933W@UYRD;x73h{^Rdq||b7i*_Pk2pgbbdX5I2L(0 zf0$B^ys{p!f));D=Z_}}hb?|kA!G&7?`4`u9oI9N)_DI){1}4ahG0GjIAdCYO;Aja z@^s|?srgMNa`QYYzCGv^8Sg5PoWOfueT<% z46H3uujZP6Vo#*#f_LE{!fvbzv|)8sCAPwkuHVG-`J}d;PI0oI%LC`|sk)rfvVs;s25CfLX>)$M&8R>YZ>;#n^k*=4Y4x7 z7PDn!!7|CYy#RQ-?pApbM(c*T?X=@GG3p0hq57k#tb*+m6DN>kY8h%Fcrjj*$^P`Z zG>`I-Nhs5YG_dF5>{#%MIsqR}ozgaC~?Bd=#1I3+z;%sxw z0%Bs~X3(i57kauzR}sDE3^DQ)jeD24F&9TTOawZR_!QAYf!6iWvkh_MhOw|oejTqwJrM&}cK95LTSfQ^9QTS0{haj@HsX1j9oD~4L1?g{ z(X1~W56&t=Jg7Lu6%R5`qR45KSF;N=__LC zr`h@&`au!;U&n%eYApS2LOAeo~BmRtxQlZ~K|TnL^b>I9UwPDeBSQOpGj#lGEqE__ zHr{-130eRZ`Q`(jFVq7H#L?5HFUPu_3eq8Rkv(2909wI zYsfWwZwG#T;Rkc*__?{DwF1vAG(Tnh`QK%L1n0#xX}HtUv-|8wwvKm0#Jd5m(+pd1uCtID=hI&?cqJE^65IF?vw zcr359dF;LPfAUa8ChzwV%n{>mg#7pIY(gNfnWuyJJ*c7=2Me1$kP-fmz?eLGHOxxj) z)bJ=mMO(wUWp0K?p9%>r(R{m!E<*!-Eb>2+H&7|8ilbFYl_zVn^+HK-sfxvR4b`;& zKQmh;tfmH-a-J~d9A0FXGv)udoZq}!pU+G=nNB$!|8L8w>gK1JRf#FCX=iP|2@7qt zHnnqFWW4xOiMFLIllOj;_viQRymKRYGthF;9bA|5_o~_w7l?|TYU6R*H1C-54&ss` z6Fu@H<(Y`_zul^inBas2(@r3Cv;=*O1Ya5nIye%1mvPk2W=HH}4Xca0Xv4pWC3vIE zOE#;(bw?AiBZ`=SJ+G;@QK@0u(qTK$W_$FuDPEmCfZHQCfTeRD!(K2L&bkj~AHD

#JoX!25cJenW)*M$#w7(%&caO$_~v2z`6ff8Hd9za{jWXikj( zBlPu1|4J+`r}DIKbHPnp4E?eQ{ne!3cT9}FV}<@t7!LY| zBJ^jGz9N=>lF+X=^n)VwzupP@sj>8fgnqiAZyTXsMf#g#=`Rra;fDV3ZH~T=lRh(+ zz7}H~?f-^;LxlcD(kI5!R||a;Lq8)z-=6fJH;%E-3qrpMhNk~V=RSnw*2jMp;3Ad8!?Z6LB-L&)=dSGh zTna17{`^@C(F$Alwu-awK~b>Wwt^;5>k9m3G4QGw_}v)z#TfYU z82F(W_?{T}#u)hO7`Sr`++M@Has_@CIBW&4u)p8Q7bDFY`Ej5@t6iQ7gAEFM0Ks=6 z&IG^9?d1!6hWz`mCUgi}EJwU>Y?Xde&=}uo@s<>UiLccTe;_T*SNx~Rj32JiTn36j?UT$Fi~Rh~erv`i88}Yfhbv_s^}sPwhTY z7u83@57ft2h{b!e0{kEuH+0n32M^;iE6uv*B{a`<9kEq#Gmt6=HOlv;{h?cudtU?L zQ=;~V@V2x+aGn-@21KWR-0Ik;ru{{$G)VK|xN7kM?|nV+Z$e?FcR_E@#A}jKbW(0* zg9fqk_l5i^97y2h>WB#$1UA&9IqPv+8NO71Q$4K@->-G_+3pYgOqCizdSd;-vuh@+ z`&mh$>|ZZ~UZFqWlo9U6zgb6rSb>kNz^{lXJ4X3}?V=PNh8ZgkAm^yfD#3%b`kc}- zZ^>^^x%fbRZ^_Tpy{cd8S*bATP!S{MBjbB7O%1gCE1^6mzDDf4Yqv=cfr^So-QS%!$!%LD|{m7oWs6S?+ zSjqHdrFoX~#5CW$9`bH7YDB*8-<=$bYx;*mTKItmq9u6{>_*qa!~pvSVGccM44 zpDSF?@8H?QeZIu4-`S`CVfBF5uHX`xl9t$LrSXz+?^_C;4UXb1nGN-|J7^?LOKfM2 z1>_HoO2O@2-jc6jv7W(O0BrS^^akJy4yUTWcuVHcfWbSE1mG_KJ%fb+&^~X~nXUw| zwxFjkcz=pN;Kz--*P^HK2k`_{sxOqA;S2WX-K*XbwhCV`*BJQ6{l>s4%X5HMpAjtm z##{QRCK`*J-H`J-eQRq|U$C3W^(M%42JY6)lsqMynJ4gF_)ZkfFQOqcF2(Tiet%sn zJ{ZR2zax@=yBcir7u@0tYziNPyu$e-)y?L74><Bq5!U*HRx!Yy9(pn65P)T-_x!KqeY zU|LeRTEDz*tfN|+v_iDCPPlR~TB9}*rg*vEKBLtsR^ZgM@GWkZ8%qVZ$!|%H3hT+5R8tJoxn-7-DA;Abf4aS(S<>h!wh|v!0 z^g}9#4#eoCG(Kbu(awqpms^RbQ9zv47&wCcJ-Gl!TyoK+S;P-(Aov3Cz^97Il^Aa` z^W9?oFxke;v>)c$4`ud4wf(Tge%Nh4c)(QiNwyz&(Uity*bh1OL%#hm*nSvcKg_is z%J9Iu9sGeU6uKH9|4)1bk?ccTk(CmNgf0XwB7QJdb z`_KN>v43^d-?jgQf9$V6V{sJzKe9o5`?ucX*gwDW@7n*1PXGA-QTl&ljQ`{M|1QV= z)vN!m{dZpWkNuC*{?8}Gw}0!Mj{Wn?|E~T2y5t}GAEo^>J@M^7dxvBH>Xm=j{`X$= zkNuC*{vTjbAYS}wUE|n4zwGbY|HF&_vHwxp{{}4P#IyhGuw(z~<$u@y-P-+Q|D&}3 z{zLKY-L&tLX;?SJ_{{(rRN&k&ZN;`;wjj{U2b{9XIMb-_RWf0X`zHr5B@*}wHR z$Nu??|E~RyYyXe^kJA2&utFEl{|ed`@7n*n^Z&8`QQH41EDpxAf9oF{`{%#^ zckO>kn}6(ol=k0-b+&l+pS{JgfA##oYyZ<*|6~87wEu&@#kYU!&5r%^=lxy#zxt2; zk9Pdi4C|b6{r_9X{?*tZ{Cn&FzyA~ekJkRL?T*;LPj27~UP*mda&E7lfz`czE4N}} ze#13Zuul@U(fxr7{lSL*gjIgLH@IXo-uc3trPe}ROUl1-T>~Rit*S2i9G|yYmvmp& zYVX5)vE`t5Ge5-xH-JC==5Xzmp^Hdwp;3Fjs}I#*!wtU3_?FwnUzxOL(H|`Bgk~pHXkmgc9f9 zm%$@K0tdUNV&BX;pXl0y4*bRg0#@)X3)?*0jm|jYq(JE}g-vTZ+V3m-1KV)JE?)A@ zOTov>a6ro3tDU0v%s=`Hn|rKBZlSIMfEp~^UOdEfZ3P1u_b<)yDMrpH#MdSxTM znO_VFjlel(VO8M|j7Y-we!L|uc!VG@3P&}tJ&McMQp|A;K!X=Iy48#3#R9msX@n?5 zOUO&d4rjhc1vc@3jjq#npTH-5`*&NR2A`7<=j`;^gaZ^ znK(SXJv<&(vg0E>m?c|*+2#kXhZX<@a_H&%bt%3;8+@b-?b-??goo+O9#VF(5LgDh3f zx*ZGNl;o%Hn6}*tjm%Kb9Q1-IIuc$@z{IsWF>YCFSb!-j=q_uhl=Udes_XuOdp=>B z`^|c9=|f-wBX^*Y%}GOkPv-y}0LP0w%a37lsKtT9_<;lBE=X?)KXVVF+G_9e-Qnf{ z%(#I0$|N7MR3MAVZs+Um&SwEoxu3z=I`Nr)A z{HQTTKb%I?-vRtL6|Cx#51}FT?fv|rAuuZXpd@1afuC3_>krqHM>|2Sbx=I5Xhm`$ ze}-Cu)L`N{cqxKs5Iev_&*XEk#aa9b3014z)){;{@i@bcF)EPrHL@c*41IE}9V@b* zbF$D|`hhM4zFNF`*nmM+ab*I}jfTe}o2j4J49_rwCN>95?e>KW9`47x3yX1wF`G%- z1y?z9NeBp-crxHeqPK=k>??)ZW;w@y>Xsaq>JHrfSn^47S(l{Ayf1$=>R zyt@K#pI0?VgGL^;{t)Y*C>yiR!8k-!Km`-q;YA4(tXvfwY;iu&M?|frKr(8b_NnQ* zUFVRGgPCzO(r#etAnPRBzx(!Fw?cf)Dv55^i-Q z(1+#Zd=}*UB-e=eVQ%l9feKR}`V==}t`(f1=q|zM^q92mWCB^`h==TEf)fr9=DBN} zN)1lnxKH66l`w=uSSnRFMuRpf6SATc}bj(`C8|Bx&|IhwWm0o-g!&(U~aKJ z{o}V$5SSi*H<5g+)Xy8>1A&#iWMHi?;iKQYV;kVO^zgO*z=L?1wnsX7V;t=9w$!~& zg|F2m%&Ix zr`ly0f01Q`8Vojmw(#IJqx9`YXd{rM%-`xDMr7XqSePO3_-$zw9;{#~t>-JQ;Jg!g zLhNDuf!Rg)u|kuzQ3F%8g*#5&0;IR(QFLZFid#_M7ibtv9M5--f-N4wk9LA1>UtOn zVBt~Kx~?Np-_*69-{rN#XOzM#&<5?e@g(K2f(xqg<6D^Z=#L%vACHH(!f0lVeo+O3 zJDA=o{<79;J%h4YD-SiWLY>C>yl-!`5)R?xMr(*|y=8HIpi%F z14H_-VfSKKLNObXw=yk7{e_bFA^6m@W2f zJ%-iV0m86a;eUF%;4DA*5XT;V1p^X(hVl+dag@%N3U`R(eE$NA{{!dyaovA+zA4&w z;Cy}X0IRT;>3DQN$LlOuF(-+>`>}RlE0IR04||(wchy+Hs{k_w=!4;RV@QhO^#Lm} z71ZJD%_N|N2K2zWHXG+Muodc853|2hd|74QhwlP;&(gAm`0@C>d1XCAeos9=eGKnG zmU8m+uTZBAWW#z-=iDaM96H~(mfGUbIjbKDxH^n=J)cfX2CX()A1f<#>Ppd+Lv!>w zSeeQC74I3e!uP~B>q!%Nrl?BI4xIGNh{Wi0j6VjKY42qDCPV0Ay&7RMHbq7&$Tux0 znwCXp4wRZ)A??$_`iM%H{WU+<3e2F0R$waaFXn~0H^QdnuxS~w!FnLC7fIZL#MMaL zg2b?S69GzMa#E$ES7R-M9#3dL^0)d6uBnsmTqC{}4(TaEj>iYf6t7D1eXM%GI+%a^ z(5kb(^G=w;lC!?>mRzX^ZtJvxbko9}Ew6%)6~u&@jV;^;6!!Q*1~MqK=8?#=UH;B0 zyZlG=v?YhLoBMB4=dLIIjIOLKM~_@xNR%Em3$#c}IBE!N^as}I_U~`C#tQHewjJ9s{N3tIkHzc>f6T_Wne*0BuY)1ID*9oQ>~lzDhU&FqY45YmD)Kix2G)X;pY3 zR)p=*>2LDPrL-w7{Ch6^Rd~kS$oRL;80wBYYq}@FqNwRcmEDsOP`alQtm>Z5pYkK< z?X^$PN7g=9tb^WFT~e8)s!KYX$$&Wb{abO$3*47#mlfxIAN@8{^q9(OR~Bro8hD10hyhMMrw^E<;es#@y18b%T~&VsMM3Yp>a+DYE{@_=pPw+i z(<$g0F)tj6g)3mIiN){{4b!~nvsBYJU95jve2pKq{!{S8s19sX7iGf&m~0GAlox$C zz<+RTGMq?{^|!pqPa|MH0~AN#;EOT2ej|j1n^!LeR8SO7Nip7zdC|vP(v%aGq_tSx zsL!X%;Ucyx+=0Qac`gt4ay_?pJ)ekYm$_jd=D_Q3e76eI)eI|mY1CvU9V1z+kV5Ed z-cQiN51_AW`8Z2sM^%3P_e}Z7 zT6g)U*IoVzuJR{j;=^9=!@Ix` zPYK>qhU{GLf`${({nJDJf!pUwB= z*W+1Trt2IHre>va|ElmtteEqeQSk@Y^Lu!XdH<7SX8*t!*k@1cxP`!eH_8e4mSOSO zd<=7wbTc20>OiBxHSfo8!nsSe!`o<50apjB-1EA)&l~`l{pL`1zge2(NSElSaH$D z%EdI2i#uld3d6@!DolIHai;qy5_8+%${;{pLFfye7`@`lu@fqMlrweIqx9S$9J<0E zx(|+jeM)IrVM8BQC<}%$ZstJWO{}NBS_aBX5wRFcDKUD2lmpR7AZ6idTS`Qgd`6Wb zI)xF{f;3^ixTfRPT{+3#R!9nOgJVqA{)%Ny3j?4ewUaJ|GVmrLalEFpUcrODKwf^& z(3pM<cr6 zgPF6D!Wj*vf4>|7c|F8YZyN91(F9uG0C%;`kVrG+YbF1i8SW&byoo(7K)# z_|%_uz*}-TQ0!}Sw8pKf7$dEP_|z2t#?k!P{J=D0Coc4b&Cqmwfe)2u1+Cu$j4smS za9;v4GQPkSbRH{fPr=!krnURTCGTBLT6&u`!Aj98EG2zhxYJ5lV}*L9W%vV_&S7B$ z>&pJn(lk9DMI|Ff)T(=5O<=j|Lwt463NB646RS{3nr@jo@WO+zNG%xpd;R6c-!s^+ z;tg?D0~WG&EIVAg!h)KrGYF_B=i>gruU_8Nx)Oz0Se32H(U!m-LNfkO#s3);EJMwB z1#cXeXyN!K>la{TLD*{j`-I4q>1~ACKOTEC(oW|Yg5VQroC-!70|LKk=kTeURvOg| ztCF-WWNB-q0czD2;yVWDh#(lFbyKDzHhsP&!Lyud>hZ@?W->nDis(;p!D9SsdP{U0 zXIy8#3^1{Rn4<)fum|D`eCccTk+Jr}I( zeGc1YLzxK}4ys;d+|%TUDLBvgGHRc0>(o91CRBzCkloE-$9mOX56AYErA-36kE5msn=+GJ+sf}TGO9d|N@ z;P2N*pDdlDn)M4gm!!=EnLjv5llx)AX*!#hJJmvUECjsJi+_AsxNZQWl6gioZ{AX! z*B3&V%KF1Q;RDtN3|mG9S(qU5J$!Foi6B;bC+r~w#yH>V{$RRky}GBLE^Xg*_83%y zZj#`Pid)Dv(&bOmLBi?vC3Zx2{BZ&az37Z(t{8$(itC7|+0%3OEHtr0TMw}#)uhj4 z@l5|%r;kn>;m_J{l8Wr41(JmR89VIGBw(=4_q6cUZUMe2CUcpc`F^D9ovJSSGtpQK z*rjd;E_@N-Nc7B1ICM6m2-~4*2}K;QttO-Y8avNh>e3q(zlAvvJ0EAu(SX+feUv$x zz73KxlE8se!g+A--@JGA!zg2VS`pCUNqTL-_&ew8*Q$mHY@itonwK@ha1Z3Ng4of< zRwEr;l{3h~H@AR;8RJFOd}%{d%rLN+V^|~sqnbb{vnKE&8n!yT0RQw`WDf$$!`zZE z1A_8!P2@RRg@IVo7ozBd>*`9lF_I9z*Lki8Kk7V}hktU4s|c@l;m0`m^6+%$xgvbE zi)M%mA1SLMJQjG@gw$T&H*+!{!hR_8*V?mjF}^QwIJXy;1;+1StBo4UaAbz7PB78e zqYMO)?P?o7635{+Jd#uQ0B%y*rRHM&pTE=a`9and2B}>cFffRNbLhJ=MrjZJaz0yz z9>5OdXmqnlG^5eQNThtccs;NkT~{9NPu?0G+_00{^cu*mmMvy*(_zX)w!wB}J4GTJ z5N4Rq5QmwTA7UYdN&D7eE;6Uc!jRls(gax-BXGd4GMiMI$wvV@`VfXq90;ef5Dtj7 z<8e|^q2mnK7^SofmZrI*#B`PjeFxjcvc+qAAiN?HRL!&P5p9aMfyd?Z)2_k>-C0JS z(fiqG_p2xL41#N)7-^*v^AIo?HprHB&*0LAyOD+a4JF+nU2f4hZdtDf4qMjubzzn> zU_At4)g280x*q!%d2iRZGN}4Ewqb>!QO|(Vhv)QX39{cZRXqO}yNWw@N^@83pYO$2 zWvk#*Ej^0T%4cZh*GY=LX(ds8^9SsXFhZ|$22X|eVcp9!?JQdHb*6WZ~UJ9LJnS_%$9%225-9@<1)g>D4W#$>dbdnw&+Kf!#Om=HmVhC`k(mNrI3bGEN zV8I_}>rUrWbnI4WAbTWg$9_fB`dB-HS?58DA0)2W+)u}54?luj4qEoJ{>X)BIj%^>UQ zh$81Z*f3`ypqc*Yt&Oq$(ZA8DfX4JkoNPw=qY>zLOpFXGEH|6^CPu+#2s;y{hwpLY zM}Zz=itq*Kztj_~2z=)TtwC9E0rjattRmu4AnbnX4QEROC;YH?Cga#}Tj#kVe3|R{ zGl#w+Y&p;6;oBXW^6(hvxgwnAJeP+{oac)0zg>9;HmEsWH5wP4BAf*46P1(_~YY z=YyqcLlBpf-HKb$YM$d|Yv4CfD~)~B3&1dP0h=D>b0@?M?Do8lL-u5XR;acD;C*i; z^C>9qUprZk#XLgrkt#O?g->CX2}R%{IEb96)?*LHz9EeKBUi&c9$~*~sc;n3>nH4<2fL`t-^erO;*qPgs{@`m9*`!Lh7PO$t?m;wJ zo{SbuAPo%s!B^PjIt0~jg8GJFnw#KlH^C3DY7R5q1dqB2RvH5I$&n)Na1+b~ffbxF z7X%Q6r)GRvn+jU%^A1+9HN#*Fx^D+N6M@^Askl4|q=jGWU3>QqoxZO;x33kbmTM-> z5k%h9nvSlgC5KBmV?PAb;$|mW!M@nR;TkIzeVY|!pVv79J$1?J5Tv+sQi4Bpd#c6d z>(GPA*z-hfW-!Vgd>!>28fjH_u`t5s;QGOSvdzkMO85efyjFF|LBqxHw@W|y9n4>B zo1)+@qrA=Zkz4$*6ISp4PE!5i6sgE{MGhW-n~R?${14KF=7(byKsla#fNFYFcb)am!^5&gmyXT@;j#AO2wnW-ZR;M^5XM z(Uk{#n+0|GICPkVXDMA|p%9o@(c=U9%^mQ|5PtfYA_P4E8pcvRD*~LtV)1IgSypH^ zpY{0H9DJy){w>#g7lN|4*|QX@Qol{Z%|!R^u^CV7W=(}T3b6+S*J26P?(T-jGxlV{ zCpynn;d`Cuif|)5M><62MC;j>+Xt&jJh+X1d%) zxBQ;LZXJA~KFJt~?#ivb*HZT(?qKj+|5|^bHTL_dCLFm+jYB;z*W*4c^}hJhVZ2{6 zsf+I1r{X7m=RT4IaJ%ByP?4dVw{WY4N1k)98zgrTmWP`{1^awLFN`8DY>d~K^=4l1 z?P}UN(8y^_hxR!#>{6#g248Sa+8Pqs&Fw2U#h)*d0@DdiQCooPEitAefX%SYOvK!B z1YmXb53JznX@dcfXFn2U)fD)oJ)@UGAAu9$g&EBI67r(!!VowQ^C|39oYesIyokBD zqJ!=-#*-qerf{{x4HBNf@`sT%CT^{Es)1U*>DX2^rE2S<)E@S}3VTO)xvgW%U8Tn~ zu5uf@%hmksa`PkQZhe7vnXU)I!N$z87IXP28ADR=KoqpwHGWpn*#Ooru{L zcSS~Ud~+Ow@d(|}QY0~liJ00-V!lp1^xWav0T?l0MiGAd-ehOLlC3nW##{OuLT~td z^fD$M>()#BO~gNge`4^TA~uHc^@CV^7vdklKQZ+0p$7~9jDKSAv$6RT{sI5Q;EyGJ zXNO+*3ZcXq+b&=f41ar&@AyT+y&f+hVva#N2Tfc-TLc=^#`2ADoI&A3GZQ z;_}NAes9*n?;IDuKoq|Zo{gvPAmKNt4u07#epylc9*oPcQ1~^igI{kKzc3HIIR4x^ zF29iQTL#*A?c)v?zo(=4ee+B_eP0oNchteJ)Wy#i#V;6_-!kEMS{?jmxcL3?yIbFm zaru2N{5F6#p1$*4{9cdZSMzi{eSZ;tW9s0y!NsqC6u)QV@=M%q+DF?u_-%9XYaGR| zM_hiV3BPam#n<<+i{B#9MEvE@w5UBB#?#!seyq%0g!63XDu4SmuTW;$4_Le&zhfbL z9V{iKVmgFv#SV*G0)d%VC(gFqH!PH-vG>Av$){pqjit`9V9JHfp23$Ju`|{;U|fWO zVQvv#K*Zc^1_n)7A}&b7ejjFGbJB`HrOrbi#qsr3h+R0epTiLj_BJhy$9XEv0uAKB z=At@nFDL7}R0Uqr;u`HlriARH9bAAw@2A(obsx)>i-S+$UBPoTUFPZEn5~MRFm8sp zAGMDo?l_7l;x2$Bj<~9b;z7(=2ss!TV6kHm#NzxjYooX1MUGQ+2MiP@n>enU#kY0N z01wOv*RDvW3{WQ@zxrVWAGqf?Sj~r!+$hlJ7_lOTccXT3f`@~%z0k{J)!g!f_h2ki zC-%pdEB2!N!JE?kt#3-zNAIvBiizJUYvsXu+(;X2F8%mgEn8#kHI=Vi+4j;qp3z47 z{7D!oi*kiB`+seWv=ZZJOz2Qisv$MTi7xx~<9UgucJUR49yC$WSXS;9Bd|vM`2#p$yvvVe{|xHWNe%zZ2)Gw-J^eQUFNT0zx$lQT6<$2kW5SW} z&mYtqQ%%ZXo$>^AfVoCsjX}S;dWgwc1JhExrNc04;ba&qRNNWIBspjt*tMu0)SP*I zZ2W3A1J|Ouo(FL^q#r_(FW3-!{@1OHT2Ei&ElI#g3N&UtohLi(c?`wTVwBv^DZT9; zrQc)7wD@959m;%dqp^HkEHOj@X&+nKt2G$ft#=W0_=%)drW}~9jtNiB$ z{^Q3#dJKpafJ4~Fu%FL+H}c%r^=!GGd%K=b#IwC$ZPo{Jdtpi7kX{?$i~IiI0>&NJ z&UD^J1h!Kl7*`9pMxa*=R&&LGYY4JpuwAbhfcXkS5jZCr@xF@$UQ55>S&D~*PpGK zI`(k zo-FoP3-I!ID0BQ5+}XqnSLW;o`h)1MU?Z}aef$yJHZB6lKXpH&JDOw%I_y$L;}$@J zVd*9^#1?ji>aBZ#U1}MBi%nnApXXB%+vkEBuN5pu%k=n`mG4U*_@7n#u+@QR&yDdi z&Fr+Buz3l;r<*15!V9y5s)nQcLo3%k8ka#pj2ILeQHw7*-E?oS$=u&r`7;heSNryF z^Wj2};vfF>tyJ}J+mjV)@VHLiQqO9ITP4=`11oa_ySsW9_^(OuE@*yDw~0*}bq#f^ zE#6tL^Tg(jtZeUj9?yMm;*&F0@gI%vZQ@LDJRzgohEg~SF2a^@&Bqe}(12_>6CiUaH7(6Ex$jf1GH5kFl!YLhCGg*y8 z!#C{|Ggs$bqyp^Ycbu121hKp&e^R(A90R9}Py?^}L$GcK=p>^Uz_o?5YGcj-7EUH- z9PRcMug1+aTf7s#0SDAUW4ug@dS-2SCyc;I!8iWT?dXBM6YekoZR(xC%Zw20wGUV; zdi0;r;fsnpF0MIYvNisXaZgV3+L}K_uC+_X0ox-0w^V-mehT*wL4UK3N#>N9;M+tRCWen%J2Q<=M zY9@DY;H>>%7sh>c7X}D+yyx-o7Z6Y^#Ox96zH#y+Uq4nkje$NY0TXT*kO4Al<9**p zhC9r!Ps4`HK~}T?CZ{+v(aj7i%n*^DQq8b zx)Pm5`4B45M5ZA-E0AnlO{Blf&t2{0r~g;mi8-DB@}u05$3Yut31`JcDJywGpRL;; zIEaH0IS>=WAlCu?AbL79sDp+2h(7%gHY~;7IVuUAK6?A5rcFim@YgVrgT&86Y=yhj z3~XZJS09~MWkiZaxbZIsTW8vtW+L7A^N_R--U$`J8;{Y=}3RRkOLaGKcDXSFI7%W2%8gTT|FN9Gj811{;YTF*!Fd+kGP!e zUbJ#Dk7npGC-(nZ>|y8+W&VaAbuGFy437uf!2ra=xYCuV22Yk4V;I2GQaQ-$VAY5D z`FcmF#Ztp)mF`LqK2(zkx0_)R<`izngBwANetYrM<^e0~V8IDiF-DrDk7L|{*9S1# zUsd=EV`dTS1VSWnFp-1u@cH;Djc^aD6{U%ZL{Fiy9$= zW&{IV`~myCU8+}Cz9y4H#CkNdM;+n7Zv^?t#Eqx#czH<%I$4Hy#$()y!DgCF zGD~L7EE;>8W^CifF~ylo1QLy|?PE8jVKjtxPp?eE$vtvGY8XRz4$dYL#Gl3M$;4ei znZf`Xc%Cm?ZC5v}qM%Fl6rbTw-WSlr62{CICUS_6H?}aJ!#Oc^H%6rFweCinp^G(Z zs!hN*C?^pG{{3*fLP3Tvi0!rjcMN|>Fle{GtAvc2p4Z;0qUEX&O zBT`^imhLTO*H2>&=HxKBXW%gGoda5ruT_QB3imm{I=+>M6S)J80x%KN?=;LusGzcG zO&n#v0?KK}iRxcLTvW$7TVqN9u1Vy|J#1KeI6C8RYKBGDr^##Mu@0|SH0ggc90D~A z*8+;@6LpT^>Qgl{zDV_BLBHf4)PdD;AYK_$z97#^@Vq$QeX~0DPAZ(pE%c^pEM`0G zH19>*_J^8gevGjLk6UBXj`w$3^ahM>pdWtJNSwJqH-OqL9RO-JFpGh0z+@9xaQ#){ zvDZfei$1TvXdQ&GLt(2*P8GK+-xbJVK8^X-o#xY+l|-S*V6qz(3eD^vU4}>fhC^%N zO?Wcn6G;1T_1PXA4%bdjX&(a&X+!u?^M7@uE#M%~miEp6M5G;bU97Y#T+%*RVWfTb zaz|Q^NSpFsq(!&rgQQW;XvW6c5m|TrOW9r7o@e`-_A{bWFrUO*cFws9`w}j1iQelA z;tlfR@>J~wUU2ef9V+~qqtX<#yGFW8HSx>U6HcHjU`Cco8>e8UgHbQiCx)v^BI8&# z71KiAL6{GJg>KKBW5Qjl`fxV-+aJgdQH*DOhOwn2hsWA<7>8g2g484omEeiWTbjrk z5Y6=Kr#51rKc+2knB*-v2db;> z6Hqg!;K`0z=r4={K$yM?52>hR9e`5wgFU{>fuzvUw}Gvt#mPpkL4ee5jGg&@VHzsl z8K2rMXXK@KW#dp2}KS#V`@)QE9mzR8&ldYKujX{ORy3p=t2%Bm_;zD=Bd74n~&VL$!VLv z@zb(Zrk^&KY3~GSIjW$+9VzYQvP8-k=4cpXdYZ-vOM6X*Bkqy%{CeWxp zvyg~trw2}`yc8(Z)0`;vEcYVTC>RSW*i`BdXo_0}+MB`Ha!^Y%fHL}4rs^*`e`?Ve zw4rnvhaYYp#JlyEhyu(Cw|1;yfp}`Z&T}$<0F-oIoUw21sKcq9Op-3Z!guMyiPWc= z1O;j~>f)c*E%hkp+t}wM;93$QF_QBk)~)MNTn`mz5i|o*dT$wPxN+u!xL@e2=LlT# zlt(g+m`x-}5i#h_ucmQKGXu|I&ZV^P~ff(2z=!#{qrif32%383Z#(${i zUAoJ`g4Rd)+c%*ez>o>7-$2Tu3`wsxV(9?PZ9<%&wM#0=AMf_7kiYwS`AI_Z`=)q+ z-Mc*O&M`?f?3D3>mvAEWELPhdc8jK=$Gn=UN&SPI4ItCGrs=*DQA^7*#IyE`keNuP z!xZU`(E`CR)ojq?GWj5veKLi8o4{q)=CUI!mlrX%YW3|Ta=VrnD=}E&(E8NQppJIhC zw!+6CJkIt94;C|-RAg+^ww?+hditR+OFLj592d)zOf+6|WU3zQ@=hox(eOPV7t2w5 zAaWs|iuWSx$HmgrZ#RNN)1cLwoDJh*>FR4l^3)3mgS$>}ME#UPH;1CLbWYYFImdZ9%dDJI=L0$?Yml5*c{#~rUY(NjU&!es2EZTkYSsdz zrSTspwC9Nqn5}{xO5_$m$u&YMTb)`#JxA_-p#vw)Td1-WInCm)rz;Mwq3GjOP5et1 zocv{>>2$Bh?I-G=H;`X(F@|jXakYY)Ce%6|mYW46Hw^RNgm-gKxA(hnnvHaWoy@y% z<8|0eI;-vEh$3`QTW)>K}xichQQ$=;m5vlF^G#xuZiigB;2AcjNX@=E9K0dXP5{ zfSuq@*aeZIQ*n%?1Rq<}PuKe?O=@HCu}OW553USX0c3wXhpOIT4*;B}A4|2tz;+)& zv2oSCYu&4%UHbMru_FtO-Fj-BRj-9&x1c6fhoQs$37`)$q#InK3Ag5LHw=x~v$KA} z8{?7X$L6FgC1Qm3%72iYILa>1!t*5^+$enMUV(J=@MP?#{Ry0S66T5fg7A@kxZZuq z3cqZH7g*tiei)b2Sm88;llPa3V=L*5i;G)%S5@pseKPb9>PqD3_(_piXKYbEZLYY$ zZa6xStR^8JtS=(rvHH$qpRg+8iV2)nO*-Ktle1L?KM*Q`^xBXgGKKYnD>D02fueHv@wX@|*GtmKv_7HhdU>u@SDQM1 zn@DZp>8u*B4sKpVVy>z&#V7Bu_#!E;4}u8@(A81=2X&Dtp1;H5H%M`FqBl<6xgl|Bc6)hf=0Ro|TW=q20}4fX{*ZGFC4hPecbRLWM07CYF??w< zz#@DFcHP-Xg&;u}W@R<}MrE_{t1q;rM9mR9LKF1d`)j!@%AJ89wZ=DjM70yHU;8*y zdo62&d5Ubo+BfhQwUY}%QTpB%$F^!qxWkG;%S3K})?F6Vjcdhh$agb!>$;%#XciH^ zrA*zwoCq@eptN=Abh4wc(C;vKFujHmNH@n9=X&X%nDh#hZp?huUiu&SK2=bX=Kg`E0=PF~B}BMK6K=rt67DuPFkyHEFnxYw0Ml(}w#L_b z3#O-E(7k)3myfUBfXTg-_Jof>SZ|W};Vv&+?>1ZEpZ##1d#`aezX@wuK!n3V# zofZDv3jbz>55C81x6V!T!}ad5R`_NghI+RJWyPLPwaPfQVNL!M<{9#*zOyIM@3YQ1 z2$w;R!QFhX5(k5`3iM@tm|BJ>%{!FBbZ9Vh45WDtxQTz1?6nfo;SDD2t!4Sk0jQ>6 z*2gi}acEV2Az`y_h2EFRLY%o^PSP_DKg6UjgBKsL`FvV_va6tRd-g3TDl=HFvH{Z= z_uc2|`+{V@&jCAQBN=vvQ1nF<;y2t&2W9SM zdYs_*!Y9d(t*%@17Elz|B4F+FP=o&h@u zmAE`^J>-=gWZUOk!c$jPqapRhgDmtH?=PExl^=|x>1ADkHfL&`q%T9dlfSO~yKsET zY!u)`bPk}@eR-C^;`V7+R~siD;p>lFe=eCz)Byl}xK#fs3$EQ~9en?o$LbIK7>X9j2y73XnUgUWi; zyDn7Ki226QG&KD!e7HR@YX$r(`AF2N-iUV>;vW({J(3UAB@}Ho`vO>>dE@Bb`#6sN zDF4oV4~gE(AARO>GVXU&CrHkduySD~;KY6hAOdzab=@_ABU#^hAiVR z&v(bnHY!IQ9;=fr>hP<(NG5rG>ZJ_ge9o*-Csv zJhEyib>dXE?UISZgdZ$_nXmu9`pXc)0^hG}GGGBvGUhYY1z7k+41}1R@otd0qB6Df zLYnuA(B~-`5%^Z%)?agNG4zlZQ`w=XMN;dVi@$;Dpl>ebWdXRkxYLXxaXzBhp(w#t zA{J$Gh%_@2yD}XY8RO=TcAe8IcFBQoi5?0WYn;yWo4LclTk+7nMT=6@Ro_tU2@`!J1>%czDGo2Eda~RE-wY7Ovb1U%rb# z0EFPn_bt0Wb*uK}GauUXi63~TOFOYG^*qxWkJ55RyEBER^&o^(#knsu?Qs1;2fxs? zp8VLJli#EzxFOUX06}8rlZ2&O$u|7CkXelbyzwnj;~=COC+qgT{BXD=BZ_BanyTVN zO#e$>6Y14hgpq@*YqqH)E+x5;$HCTpMJwdEKBl&O4nvBfP0jS1zj9wavKPLHle_9;d)Yv&FkL%LmYnV7{9V$st zqrf=C)LeXo6E_~6qRvE&lQnrtF`~v za87NN^#590z8NHhUl=wyH>A7i|^6`NEGCy|DMv+>f;uvL-RrE76T z{{}#t(lAir9gD3DhV~pdSA9Glh+cX6WJd5t>BK8F2w6`h1<$1gPvELYSt=pd97GaN zjR&E7|N2uXJ+;>g>K2u&Pz6?&uOSP~;&{VQl=N7omWh>X~g~ z8Y<^9gg%-Gq^CUqpe^?U`lZmiQ|U3i0`6Zqd~9pEWovwCp)<&*G4lJje`}eF26_ z{ra;nXNIA@Ymnj%u}MigNZ5}Kf$<{ ztwb(j0>>NhR&SyJ!x(cYkGS;57Gvoc4!NRi<8imOKW9^z?)0UG>Y^x;<9N9jGpao5 z%Z2K2^*W>*p6}&2;q$%qt)K4|eO<<@W8Uw@>)lui->y(1od0Gh%3)I+KNVMzmUT7p zoa$GZSA0Kp;+5<_fic`w3|se~|)0IueOJBl&`Ne3j%t zNvPZU9p6eB(D40-xi*D6U!p+Ep|rGkYF^8+DGfV!2q$iWR^9DneBq5fEDP~Q=>Wv! zW+H|ML2@o3g4!HmTg~S5=`}n?60j9aumu2MNP4)>Fud3MyR6Knq=n;?U>%9@$RSNp zu#@f!qX2P?Po782qvesW9Yo^Ku$P=~aRO>ehPoN|_$6N;^~__yal02tU3URy$r2RD z46VlDgEuhkip!%K=V`d)T`RrBy%e=L@x>KHDTsvEDd$%+4BzgfSTa6^&QEd|$z(?E zt0TojiWiiOX5>yf5^KiR)(eI0GBTBeC25B2@?$9jt#%FL;ruVe$8uz}T7noj>BCm| z(riuxO{y=RU$L)&8QC6QaAutQO;oJd1bHJ{8(9#h8QSN1K>ic_c97s?$VJ_-(h!M`E?GNK&sTmLsic zkZpYr%NB^S+Uea=I0$N9q-QOCyu-0&svd5A!_Sv->T8GPyFqNjd3gg~o>Gs2Q^>~z zNX{4H&B`q>L&rY!E+I;XR*2_`=_EFr4Z&I#OGKdaMPgM2RQ+OAdHAuUY|$!sXxu-7 zEcm+?j1YI~bvZ5$o9zKS_=Qp>EKHB_FThTb#NtwXyO=>Rg%qR|ivJ}ZGh`ESX;00r z-AQir>QT|}=Ur2wvXB>i6moqZ|K|&ph=1LQ%XXB`J zW+=+{9-Ng`(8OAhJ4H@RtNWOrS1`t~GyXjfKo4KUu?UZ{D%MO(QfyV>LP$N#*MJnY z^}^xnWm|EouMo42)Ym`uLy2nHN+=gkMycTt3p395teP|Q_&<-}N_H@<_J6?B2AQ}< zHd~%vY7!w>Q-PM`21qU(9!6Y6zo&m89;A-I;gDR^5s0e&!4}icivS0boUyp=BVB%XKPAz;Tu+suAaVxa+J`8*d{2tXI$e zt35Jo9nOBu&qGb&#FQMhRu)G^t9(ulRo%lbCC0Aheb(N>W)$C3z2dn zSGgkWTqp6L3W7}Y{dZeIAuw9^mX~{AF&OV?Bm>7I9!$(8X?h9+M`DX}@TFdQmeN?C zq0_aJPuHNhXfstMQbb0o`X};YLWw;>%xqD;MuKFCDh^t4AB6H#mB%{w0!@%mjWD3p z_rFs$s&bfm!kwh_-8Bfa;Z=YgO+L=t7(t9i3|g;hh7=D&NuW69wQM?XIHsSFK!#ct zNmR|{4_|PU5Z5HQ>i#jklMMldR3x!_)suk5A0!y$x&86Uj+`??`t|(7x?ksxKyrM3 z33J64OOFcfW=^jwI1eq{>i1y-AVeRYY1@abrVq`?LCE=wN(q$i5{sIy0BU{9l+_at z@~Z)W1^H-!XN#IST_E35qanZd{5FtJ!?gl7lF}si;NtyWqvT{QA)Ja;&7N2reG1<$)B$wYmeb7?_la&8TqCEnGLXeY36sxBK&@ z`wHJ%0&Ror2M+Unb`Mxb!EUU~f70(m_)gz7s?VVg3V{ zP=;eOh3>bC?=seYZ)vtw1QQJtV3d`h8Q}%(=J>k2T^Ktz$H@

jLyiPQ3n2@&?Ue5L>f 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

hzf$DfGa2| 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

SL?%tKlLs6wK%4S>`| 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

-NM`M__{V*pG5q{Zhz< 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%0AP*7|h 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ᗙBXD+_$ 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

l9_V#WoQ3}&5Uu4TB# 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 z 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 z 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
T{!sN+4)a1Tc1m9MONef@ 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%0D4}h#-qIlVq+k715%!@6f(D`dkwzN5cw^nyBFq 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≺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

y#l&e&?1zk5?tx7mYw&4%KiweR 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 We@3tW(La96Z$IiARbK{gpZ$L|({6nL literal 0 HcmV?d00001 diff --git a/qa/L0_response_cache/test.sh b/qa/L0_response_cache/test.sh index bb372dab9f..c99aac89d1 100755 --- a/qa/L0_response_cache/test.sh +++ b/qa/L0_response_cache/test.sh @@ -381,107 +381,6 @@ ERROR_MESSAGE="\n***\n*** Failed: Request added to cache successfully when it wa CACHE_SIZE=200 test_response_cache_ensemble_model "${TEST_NAME}" "${ERROR_MESSAGE}" - -############### Response Cache Memory Growth Test ############### - -# Set server, client and valgrind arguments -LEAKCHECK=/usr/bin/valgrind -MASSIF_TEST=../common/check_massif_log.py -MODEL="identity_cache" -LEAKCHECK_LOG="${MODEL}.valgrind.log" -MASSIF_LOG="${MODEL}.valgrind.massif" -GRAPH_LOG="memory_growth_${MODEL}.log" -SERVER_LOG="${MODEL}.server.log" -CLIENT_LOG="${MODEL}_PA.client.log" -RANDOM_DATA_CLIENT_LOG="${MODEL}_random_data_script.log" -RANDOM_DATA_JSON="`pwd`/random_inputs.json" -RANDOM_DATA_GENERATOR="generate_random_data.py" - -LEAKCHECK_ARGS="--tool=massif --time-unit=B --massif-out-file=$MASSIF_LOG --max-threads=3000 --log-file=$LEAKCHECK_LOG" -SERVER_ARGS="--model-repository=`pwd`/models --model-control-mode=explicit --load-model=${MODEL} --cache-config=local,size=10485760" # 10MB cache - -set +e -# Generate random data for perf_analyzer requests to fill the cache and maximize cache misses -python "$RANDOM_DATA_GENERATOR" --num-inputs=10000 --batch-size=1 --output-file="${RANDOM_DATA_JSON}" >> "$RANDOM_DATA_CLIENT_LOG" 2>&1 -if [ $? -ne 0 ]; then - cat "$RANDOM_DATA_CLIENT_LOG" - echo -e "\n***\n*** Failed to run ${RANDOM_DATA_GENERATOR}.\n***" - RET=1 - exit 1 -else - # Check if the JSON data file was generated - if [ ! -f "${RANDOM_DATA_JSON}" ]; then - echo -e "\n***\n*** FAILED - JSON data file was not found at the expected path: ${RANDOM_DATA_JSON}\n***" - RET=1 - exit 1 - fi -fi -set -e - -# Run the server -run_server_leakcheck -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 -fi - -pip3 install perf_analyzer - -TEMP_RET=0 -REPETITION=10 -CONCURRENCY=20 -CLIENT_BS=1 -PERF_ANALYZER=perf_analyzer -TEMP_CLIENT_LOG=temp_client.log - -set +e -SECONDS=0 -# Run the perf analyzer 'REPETITION' times -for ((i=1; i<=$REPETITION; i++)); do - # Use random data to ensure cache misses - $PERF_ANALYZER -v -m $MODEL --shape=INPUT0:1024 -i grpc --concurrency-range $CONCURRENCY -b $CLIENT_BS -p 20000 --input-data="${RANDOM_DATA_JSON}" > $TEMP_CLIENT_LOG 2>&1 - PA_RET=$? - cat $TEMP_CLIENT_LOG >> $CLIENT_LOG - # Success - if [ ${PA_RET} -eq 0 ]; then - continue - # Unstable measurement: OK for this test - elif [ ${PA_RET} -eq 2 ]; then - continue - # Other failures unexpected, report error - else - echo -e "\n***\n*** perf_analyzer for $MODEL failed on iteration $i\n***" >> $CLIENT_LOG - RET=1 - fi -done -TEST_DURATION=$SECONDS -set -e - -# Stop Server -kill $SERVER_PID -wait $SERVER_PID - -set +e - -# Log test duration and the graph for memory growth -MAX_ALLOWED_ALLOC=2 # MB -hrs=$(printf "%02d" $((TEST_DURATION / 3600))) -mins=$(printf "%02d" $(((TEST_DURATION / 60) % 60))) -secs=$(printf "%02d" $((TEST_DURATION % 60))) -echo -e "Test Duration: $hrs:$mins:$secs (HH:MM:SS)" >> ${GRAPH_LOG} -ms_print ${MASSIF_LOG} | head -n35 >> ${GRAPH_LOG} -cat ${GRAPH_LOG} -# Check the massif output -python $MASSIF_TEST $MASSIF_LOG $MAX_ALLOWED_ALLOC --start-from-middle >> $GRAPH_LOG 2>&1 -if [ $? -ne 0 ]; then - echo -e "\n***\n*** Memory growth test for $MODEL Failed.\n***" - RET=1 -fi -# Always output memory usage for easier triage of MAX_ALLOWED_ALLOC settings in the future -grep -i "Change in memory allocation" "${GRAPH_LOG}" || true -set -e - if [ $RET -eq 0 ]; then echo -e "\n***\n*** Test Passed\n***" else diff --git a/qa/L0_sagemaker/sagemaker_request_many_chunks.py b/qa/L0_sagemaker/sagemaker_request_many_chunks.py deleted file mode 100755 index e7ef8dd686..0000000000 --- a/qa/L0_sagemaker/sagemaker_request_many_chunks.py +++ /dev/null @@ -1,173 +0,0 @@ -#!/usr/bin/python -# Copyright 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import socket -import sys -import unittest - -sys.path.append("../common") -from test_util import MIB, get_server_process_from_env, wait_for_stable_rss - - -class SagemakerRequestManyChunksTest(unittest.TestCase): - def setUp(self): - self._local_host = "localhost" - self._sagemaker_port = 8080 - # Must match server kMaxChunkedChunks (http_server.cc). - self._k_max_chunked_chunks = 65536 - self._over_max_chunks_error = f"Chunked request body exceeds maximum of {self._k_max_chunked_chunks} non-empty chunks. Send fewer or larger HTTP chunks." - - def _sagemaker_chunked_header(self): - return ( - f"POST /models HTTP/1.1\r\n" f"X-Amzn-SageMaker-Target-Model: ZZZZZZZ\r\n" - ) - - def send_chunked_request( - self, - header: str, - chunk_count: int, - expected_response: str, - expected_http_status=400, - ): - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - header = ( - f"{header}" - f"Host: {self._local_host}:{self._sagemaker_port}\r\n" - f"Content-Type: application/octet-stream\r\n" - f"Transfer-Encoding: chunked\r\n" - f"Connection: close\r\n" - f"\r\n" - ) - try: - s.connect((self._local_host, self._sagemaker_port)) - # HTTP request with chunked encoding - s.sendall((header.encode())) - - # Send chunked payload (server may close early when over chunk limit) - for _ in range(chunk_count): - try: - s.send(b"1\r\nA\r\n") - except (BrokenPipeError, ConnectionResetError): - break - try: - s.sendall(b"0\r\n\r\n") - except (BrokenPipeError, ConnectionResetError): - # Server may close/reset early after detecting chunk-limit violation. - # In that case, failing to send the terminating chunk is expected. - pass - - # Receive response - response = b"" - while True: - try: - chunk = s.recv(4096) - if not chunk: - break - response += chunk - except ConnectionResetError: - break - except socket.timeout: - break - self.assertTrue( - response, - "expected error response body, but socket closed/reset before any bytes", - ) - status_line = response.split(b"\r\n", 1)[0].decode(errors="replace") - self.assertTrue( - status_line.startswith(f"HTTP/1.1 {expected_http_status} "), - f"expected HTTP status {expected_http_status}, got {status_line!r}", - ) - self.assertIn(expected_response, response.decode()) - except Exception as e: - raise (e) - finally: - s.close() - - def test_chunked_at_max_chunks(self): - self.send_chunked_request( - self._sagemaker_chunked_header(), - self._k_max_chunked_chunks, - "failed to parse the request JSON buffer: Invalid value. at 0", - ) - - def test_chunked_rejected_over_max_chunks(self): - self.send_chunked_request( - self._sagemaker_chunked_header(), - self._k_max_chunked_chunks + 1, - self._over_max_chunks_error, - ) - - def test_chunked_over_max_chunks_reject_with_bounded_rss_growth(self): - many_chunks = 1000000 - - # verify server is running - server = get_server_process_from_env("SERVER_PID") - self.assertTrue(server.is_running()) - - # warm up and wait until RSS is stable. - self.send_chunked_request( - self._sagemaker_chunked_header(), - 1000000, # way over max chunks - self._over_max_chunks_error, - ) - # Wait until RSS is stable across several measurements before continuing. - server = get_server_process_from_env("SERVER_PID") - wait_for_stable_rss(server) - - # Monitor RSS growth over 100 requests. - repeat_request_count = 100 - rss_before = server.memory_info().rss - # TODO: Why sagemaker server occasionally grows >10 MiB but http server always smaller than 1 MiB? - max_rss_growth_bytes = 20 * MIB - - for _ in range(repeat_request_count): - self.send_chunked_request( - self._sagemaker_chunked_header(), - many_chunks, # way over max chunks - self._over_max_chunks_error, - ) - - rss_after = server.memory_info().rss - growth = rss_after - rss_before - print( - f"RSS: before={rss_before / MIB:.1f} MiB, " - f"after={rss_after / MIB:.1f} MiB, " - f"growth={growth / MIB:.1f} MiB, " - f"limit={max_rss_growth_bytes / MIB:.0f} MiB", - flush=True, - ) - self.assertLess( - growth, - max_rss_growth_bytes, - f"Server RSS grew by {growth / MIB:.1f} MiB after " - f"{repeat_request_count} over-limit chunked infer requests " - f"(limit {max_rss_growth_bytes / MIB:.0f} MiB).", - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/qa/L0_sagemaker/test.sh b/qa/L0_sagemaker/test.sh index 61ed1b3b48..dde4c794d8 100755 --- a/qa/L0_sagemaker/test.sh +++ b/qa/L0_sagemaker/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2021-2026, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2021-2025, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -327,7 +327,7 @@ fi # Helper library to parse SSE events # https://github.com/mpetazzoni/sseclient -pip install sseclient-py psutil +pip install sseclient-py # Inference with generate_stream inference type set +e @@ -554,14 +554,6 @@ else RET=1 fi fi - -# Verify that loading without X-Amzn-SageMaker-Target-Model header logs the model name. -grep "Loading SageMaker TargetModel: sm_mme_model_1" $SERVER_LOG -if [ $? -ne 0 ]; then - echo -e "\n***\n*** Failed. Expected TargetModel log with model name\n***" - cat $SERVER_LOG - RET=1 -fi set -e unset SAGEMAKER_MULTI_MODEL @@ -573,255 +565,8 @@ kill $SERVER_PID wait $SERVE_PID # MME end -### Test Sagemaker Requests Containing Many Chunks ### -rm -rf models && mkdir models && \ - cp -r $DATADIR/qa_model_repository/onnx_int32_int32_int32 models/sm_model && \ - rm -r models/sm_model/2 && rm -r models/sm_model/3 && \ - sed -i "s/onnx_int32_int32_int32/sm_model/" models/sm_model/config.pbtxt - -export SAGEMAKER_TRITON_DEFAULT_MODEL_NAME=sm_model -REQUEST_MANY_CHUNKS_PY="sagemaker_request_many_chunks.py" -CLIENT_LOG="./client.sagemaker_request_many_chunks.log" -SERVER_LOG="./server.sagemaker_request_many_chunks.log" - -serve > $SERVER_LOG 2>&1 & -SERVE_PID=$! -# Obtain Triton PID in such way as $! will return the script PID -sleep 1 -SERVER_PID=`ps | grep tritonserver | awk '{ printf $1 }'` -sagemaker_wait_for_server_ready $SERVER_PID 10 -if [ "$WAIT_RET" != "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - kill $SERVER_PID || true - cat $SERVER_LOG - exit 1 -fi - -# Ping -set +e -code=`curl -s -w %{http_code} -o ./ping.out localhost:8080/ping` -set -e -if [ "$code" != "200" ]; then - cat ./ping.out - echo -e "\n***\n*** Test Failed\n***" - RET=1 -fi - -set +e -SERVER_PID=$SERVER_PID python $REQUEST_MANY_CHUNKS_PY >>$CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - echo -e "\n***\n*** Sagemaker Request Many Chunks Test Failed\n***" - cat $SERVER_LOG - cat $CLIENT_LOG - RET=1 -fi -set -e - -kill $SERVER_PID -wait $SERVE_PID - -### Restricted API regression for SageMaker endpoint ### -# Verify that --http-restricted-api applies to SageMaker model management -# endpoints (load, unload, list, get) while leaving health and inference -# unrestricted. - -SERVER_LOG="./sagemaker_restricted_api_server.log" -SERVER_ARGS="--allow-sagemaker=true --allow-http=true \ - --allow-grpc=false --allow-metrics=false \ - --model-repository=`pwd`/models \ - --model-control-mode=explicit \ - --load-model=sm_model \ - --http-restricted-api=model-repository:X-SM-Auth=secret" -run_server_nowait -sagemaker_wait_for_server_ready $SERVER_PID 10 -if [ "$WAIT_RET" != "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - kill $SERVER_PID || true - cat $SERVER_LOG - exit 1 -fi - -set +e - -# Health should succeed without restricted header -rm -f ./curl.out -code=`curl -s -w %{http_code} -o ./curl.out localhost:8080/ping` -if [ "$code" != "200" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected /ping to succeed without restricted header\n***" - RET=1 -fi - -# Inference should succeed without restricted header -rm -f ./curl.out -code=`curl -s -w %{http_code} -o ./curl.out -X POST localhost:8080/invocations \ - -H "Content-Type: application/json" \ - -d '{"inputs":[{"name":"INPUT0","datatype":"INT32","shape":[1,16],"data":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]},{"name":"INPUT1","datatype":"INT32","shape":[1,16],"data":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]}]}'` -if [ "$code" != "200" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected /invocations inference to succeed without restricted header\n***" - RET=1 -fi - -# List models without auth header should be blocked -rm -f ./curl.out -code=`curl -s -w %{http_code} -o ./curl.out localhost:8080/models` -if [ "$code" != "403" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected GET /models to return 403 without restricted header (got $code)\n***" - RET=1 -else - grep "This API is restricted" ./curl.out - if [ $? -ne 0 ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected restriction error message in response body\n***" - RET=1 - fi -fi - -# Get model without auth header should be blocked -rm -f ./curl.out -code=`curl -s -w %{http_code} -o ./curl.out localhost:8080/models/sm_model` -if [ "$code" != "403" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected GET /models/ to return 403 without restricted header (got $code)\n***" - RET=1 -fi - -# Load model without auth header should be blocked -rm -f ./curl.out -code=`curl -s -w %{http_code} -o ./curl.out -X POST localhost:8080/models \ - -H "Content-Type: application/json" \ - -d '{"model_name":"test","url":"/opt/ml/models/123/model"}'` -if [ "$code" != "403" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected POST /models (load) to return 403 without restricted header (got $code)\n***" - RET=1 -fi - -# Unload model without auth header should be blocked -rm -f ./curl.out -code=`curl -s -w %{http_code} -o ./curl.out -X DELETE localhost:8080/models/sm_model` -if [ "$code" != "403" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected DELETE /models (unload) to return 403 without restricted header (got $code)\n***" - RET=1 -fi - -# List models WITH correct auth header should succeed -rm -f ./curl.out -code=`curl -s -w %{http_code} -o ./curl.out -H "X-SM-Auth: secret" localhost:8080/models` -if [ "$code" == "403" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected GET /models with auth header to pass restriction check\n***" - RET=1 -fi - -# Get model WITH correct auth header should pass restriction check -rm -f ./curl.out -code=`curl -s -w %{http_code} -o ./curl.out -H "X-SM-Auth: secret" localhost:8080/models/sm_model` -if [ "$code" == "403" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected GET /models/ with auth header to pass restriction check\n***" - RET=1 -fi - -# Wrong auth header value should be rejected -rm -f ./curl.out -code=`curl -s -w %{http_code} -o ./curl.out -H "X-SM-Auth: wrong" localhost:8080/models` -if [ "$code" != "403" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected wrong auth header value to return 403 (got $code)\n***" - RET=1 -fi - -# Verify core HTTP endpoint is also restricted by the same flag -rm -f ./curl.out -code=`curl -s -w %{http_code} -o ./curl.out -X POST localhost:8000/v2/repository/index` -if [ "$code" != "403" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected core HTTP repository index to be restricted (got $code)\n***" - RET=1 -fi - -set -e - -kill $SERVER_PID -wait $SERVER_PID - -### HTTP max input size enforcement on SageMaker endpoint ### -# Verify that --http-max-input-size is enforced on the SageMaker /invocations -# path, not just the core HTTP endpoint. - -rm -rf models_identity -mkdir -p models_identity/sm_identity/1 && \ - cp ../python_models/identity_fp32/model.py models_identity/sm_identity/1/ && \ - cp ../python_models/identity_fp32/config.pbtxt models_identity/sm_identity/ && \ - sed -i "s/identity_fp32/sm_identity/" models_identity/sm_identity/config.pbtxt -mkdir -p /opt/ml -ln -sf `pwd`/models_identity /opt/ml/model - -export SAGEMAKER_TRITON_DEFAULT_MODEL_NAME=sm_identity -SERVER_LOG="./sagemaker_max_input_size_server.log" -SERVER_ARGS="--allow-sagemaker=true --allow-http=true \ - --allow-grpc=false --allow-metrics=false \ - --model-repository=`pwd`/models_identity \ - --http-max-input-size=128" -run_server_nowait -sagemaker_wait_for_server_ready $SERVER_PID 10 -if [ "$WAIT_RET" != "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - kill $SERVER_PID || true - cat $SERVER_LOG - exit 1 -fi - -set +e - -# Small payload under 128 bytes should succeed -rm -f ./curl.out -code=`curl -s -w %{http_code} -o ./curl.out -X POST localhost:8080/invocations \ - -H "Content-Type: application/json" \ - -d '{"inputs":[{"name":"INPUT0","datatype":"FP32","shape":[1,1],"data":[1.0]}],"outputs":[{"name":"OUTPUT0"}]}'` -if [ "$code" != "200" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected small payload to succeed on SageMaker endpoint (got $code)\n***" - RET=1 -fi - -# Large payload over 128 bytes should be rejected -rm -f ./curl.out -LARGE_PAYLOAD='{"inputs":[{"name":"INPUT0","datatype":"FP32","shape":[1,16],"data":[1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0,14.0,15.0,16.0]}],"outputs":[{"name":"OUTPUT0"}]}' -code=`curl -s -w %{http_code} -o ./curl.out -X POST localhost:8080/invocations \ - -H "Content-Type: application/json" \ - -d "$LARGE_PAYLOAD"` -if [ "$code" == "200" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected oversized payload to be rejected on SageMaker endpoint\n***" - RET=1 -fi - -# Same limit should apply to core HTTP endpoint -rm -f ./curl.out -code=`curl -s -w %{http_code} -o ./curl.out -X POST localhost:8000/v2/models/sm_identity/infer \ - -H "Content-Type: application/json" \ - -d "$LARGE_PAYLOAD"` -if [ "$code" == "200" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected oversized payload to be rejected on core HTTP endpoint\n***" - RET=1 -fi - -set -e - -unset SAGEMAKER_TRITON_DEFAULT_MODEL_NAME - -kill $SERVER_PID -wait $SERVER_PID - unlink /opt/ml/model rm -rf /opt/ml/model -rm -rf models_identity if [ $RET -eq 0 ]; then echo -e "\n***\n*** Test Passed\n***" diff --git a/qa/L0_sdk/test.sh b/qa/L0_sdk/test.sh index 84c6f429fb..20baf31639 100755 --- a/qa/L0_sdk/test.sh +++ b/qa/L0_sdk/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -34,13 +34,17 @@ set +e RET=0 -# Check image_client and perf_analyzer +# Check image_client and perf_client if [[ ! -x "triton_client/bin/image_client" ]]; then echo -e "*** image_client executable not present\n" RET=1 fi -if ! command -v perf_analyzer >/dev/null 2>&1; then - echo -e "*** perf_analyzer is not installed\n" +if [[ ! -x "triton_client/bin/perf_analyzer" ]]; then + echo -e "*** perf_analyzer executable is not present\n" + RET=1 +fi +if [[ ! -x "triton_client/bin/perf_client" ]]; then + echo -e "*** perf_client link is not present\n" RET=1 fi @@ -153,9 +157,11 @@ fi # we need to replace the text here as well to match the normalized version. WHLVERSION=`cat /workspace/TRITON_VERSION | sed 's/dev/\.dev0/'` if [[ "aarch64" != $(uname -m) ]] ; then - WHLS="tritonclient-${WHLVERSION}-py3-none-any.whl" + WHLS="tritonclient-${WHLVERSION}-py3-none-any.whl \ + tritonclient-${WHLVERSION}-py3-none-manylinux1_x86_64.whl" else - WHLS="tritonclient-${WHLVERSION}-py3-none-any.whl" + WHLS="tritonclient-${WHLVERSION}-py3-none-any.whl \ + tritonclient-${WHLVERSION}-py3-none-manylinux2014_aarch64.whl" fi for l in $WHLS; do if [[ ! -f "triton_client/python/$l" ]]; then @@ -173,7 +179,7 @@ python -c """import tritonclient; import tritonclient.grpc; import tritonclient. import tritonclient.utils.cuda_shared_memory; import tritonclient.utils.shared_memory""" RET=$(($RET+$?)) -EXECUTABLES="perf_analyzer" +EXECUTABLES="perf_analyzer perf_client" for l in $EXECUTABLES; do if [ $(which -a $l | grep "/usr/local/bin/$l" | wc -l) -ne 1 ]; then which -a $l diff --git a/qa/L0_sequence_batcher/test.sh b/qa/L0_sequence_batcher/test.sh index 5df80b9b90..229a935319 100755 --- a/qa/L0_sequence_batcher/test.sh +++ b/qa/L0_sequence_batcher/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -115,9 +115,6 @@ else fi SERVER_ARGS_EXTRA="--backend-directory=${BACKEND_DIR} --log-verbose=1" -if [ "$TEST_SYSTEM_SHARED_MEMORY" -eq 1 ] || [ "$TEST_CUDA_SHARED_MEMORY" -eq 1 ]; then - SERVER_ARGS_EXTRA="${SERVER_ARGS_EXTRA} --allow-client-shm=true" -fi source ../common/util.sh diff --git a/qa/L0_sequence_stress/test.sh b/qa/L0_sequence_stress/test.sh index cb6e0328c8..b2bc66f8ac 100755 --- a/qa/L0_sequence_stress/test.sh +++ b/qa/L0_sequence_stress/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2021, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -85,45 +85,6 @@ for model_trial in 1 2 4 ; do wait $SERVER_PID done -# Test invalid gRPC infer handler thread count -for thread_cnt in -1 0 1 129; do - MODEL_DIR=models1 - SERVER_ARGS="--model-repository=`pwd`/$MODEL_DIR --grpc-infer-thread-count=$thread_cnt" - SERVER_LOG="./$MODEL_DIR.server.log" - run_server - if [ "$SERVER_PID" != "0" ]; then - echo -e "\n***\n*** Failed: $SERVER started successfully when it was expected to fail\n***" - RET=1 - kill SERVER_PID - wait $SERVER_PID - fi -done - -# Test gRPC infer handler thread count under stress -thread_cnt=128 -for model_trial in 1 2 4 ; do - MODEL_DIR=models${model_trial} - SERVER_ARGS="--model-repository=`pwd`/$MODEL_DIR --grpc-infer-thread-count=$thread_cnt" - SERVER_LOG="./$MODEL_DIR.server.log" - run_server - if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 - fi - - set +e - python $STRESS_TEST >>$CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - echo -e "\n***\n*** Test Failed\n***" - RET=1 - fi - set -e - - kill $SERVER_PID - wait $SERVER_PID -done - if [ $RET -eq 0 ]; then echo -e "\n***\n*** Test Passed\n***" else diff --git a/qa/L0_shared_memory/shared_memory_test.py b/qa/L0_shared_memory/shared_memory_test.py index 51bf914c88..8f3c2fbb52 100755 --- a/qa/L0_shared_memory/shared_memory_test.py +++ b/qa/L0_shared_memory/shared_memory_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -47,7 +47,6 @@ class SystemSharedMemoryTestBase(tu.TestResultCollector): DEFAULT_SHM_BYTE_SIZE = 64 - SYS_PAGE_SIZE = os.sysconf("SC_PAGE_SIZE") def setUp(self): self._setup_client() @@ -114,6 +113,10 @@ def _configure_server( shm_op1_handle, ] # Implicit assumption that input and output byte_sizes are 64 bytes for now + input0_data = np.arange(start=0, stop=16, dtype=np.int32) + input1_data = np.ones(shape=16, dtype=np.int32) + shm.set_shared_memory_region(shm_ip0_handle, [input0_data]) + shm.set_shared_memory_region(shm_ip1_handle, [input1_data]) self.triton_client.register_system_shared_memory( "input0_data", "/input0_data", register_byte_size, offset=register_offset ) @@ -126,16 +129,6 @@ def _configure_server( self.triton_client.register_system_shared_memory( "output1_data", "/output1_data", register_byte_size, offset=register_offset ) - - # Write data to shared memory regions - input0_data = np.arange(start=0, stop=16, dtype=np.int32) - input1_data = np.ones(shape=16, dtype=np.int32) - shm.set_shared_memory_region( - shm_ip0_handle, [input0_data], offset=register_offset - ) - shm.set_shared_memory_region( - shm_ip1_handle, [input1_data], offset=register_offset - ) self.shm_names = ["input0_data", "input1_data", "output0_data", "output1_data"] def _cleanup_shm_handles(self): @@ -145,39 +138,6 @@ def _cleanup_shm_handles(self): class SharedMemoryTest(SystemSharedMemoryTestBase): - def test_client_shm_disabled_by_default(self): - # When the server is started without --allow-client-shm, registration and - # unregistration are rejected but querying status remains allowed (empty). - shm_op0_handle = shm.create_shared_memory_region("dummy_data", "/dummy_data", 8) - self._shm_handles.append(shm_op0_handle) - - shm_status_before = self.triton_client.get_system_shared_memory_status() - if self.protocol == "http": - self.assertEqual(len(shm_status_before), 0) - else: - self.assertEqual(len(shm_status_before.regions), 0) - - with self.assertRaisesRegex( - utils.InferenceServerException, - "Client shared memory is disabled", - ): - self.triton_client.register_system_shared_memory( - "dummy_data", "/dummy_data", 8 - ) - - with self.assertRaisesRegex( - utils.InferenceServerException, - "Client shared memory is disabled", - ): - self.triton_client.unregister_system_shared_memory("dummy_data") - - shm_status_after = self.triton_client.get_system_shared_memory_status() - self.assertEqual( - shm_status_before, - shm_status_after, - "system shared memory status must be unchanged after failed register/unregister", - ) - def test_invalid_create_shm(self): with self.assertRaisesRegex( shm.SharedMemoryException, "unable to create the shared memory region" @@ -332,45 +292,6 @@ def test_too_big_shm(self): self._shm_handles.append(shm_ip2_handle) self._cleanup_shm_handles() - def test_large_shm_register_offset(self): - # Test for out of bounds read vulnerability when registering system shared memory with large offset - - platforms = ( - ["python", "onnx", "libtorch", "plan", "openvino"] - if os.environ.get("BACKENDS") is None - else os.environ.get("BACKENDS").split() - ) - for platform in platforms: - model_name = f"{platform}_int32_int32_int32" - - # Test for large offset - error_msg = [] - # Create a large shm size (page_size * 1024 is large enough to reproduce a segfault). - # Register offset at 1 page before the end of the shm region to give enough space for the input/output data. - create_byte_size = self.SYS_PAGE_SIZE * 1024 - register_offset = self.SYS_PAGE_SIZE * 1023 - self._configure_server( - create_byte_size=create_byte_size, - register_offset=register_offset, - ) - - iu.shm_basic_infer( - self, - self.triton_client, - self._shm_handles[0], - self._shm_handles[1], - self._shm_handles[2], - self._shm_handles[3], - error_msg, - register_offset=register_offset, - protocol=self.protocol, - use_system_shared_memory=True, - override_model_name=model_name, - ) - self.triton_client.unregister_system_shared_memory() - if len(error_msg) > 0: - raise Exception(str(error_msg)) - def test_mixed_raw_shm(self): # Mix of shared memory and RAW inputs error_msg = [] @@ -411,12 +332,7 @@ def test_unregisterall(self): def test_infer_offset_out_of_bound(self): # Shared memory offset outside output region - Throws error error_msg = [] - create_byte_size = self.SYS_PAGE_SIZE + self.DEFAULT_SHM_BYTE_SIZE - register_offset = self.SYS_PAGE_SIZE - self._configure_server( - create_byte_size=create_byte_size, - register_offset=register_offset, - ) + self._configure_server() if self.protocol == "http": # -32 when placed in an int64 signed type, to get a negative offset # by overflowing @@ -446,13 +362,8 @@ def test_infer_offset_out_of_bound(self): def test_infer_byte_size_out_of_bound(self): # Shared memory byte_size outside output region - Throws error error_msg = [] - create_byte_size = self.SYS_PAGE_SIZE + self.DEFAULT_SHM_BYTE_SIZE - register_offset = self.SYS_PAGE_SIZE - self._configure_server( - create_byte_size=create_byte_size, - register_offset=register_offset, - ) - offset = 1 + self._configure_server() + offset = 60 byte_size = self.DEFAULT_SHM_BYTE_SIZE iu.shm_basic_infer( @@ -474,59 +385,6 @@ def test_infer_byte_size_out_of_bound(self): ) self._cleanup_shm_handles() - def test_infer_integer_overflow(self): - # Test for integer overflow vulnerability in offset + byte_size calculation - error_msg = [] - self._configure_server() - - offset = 32 - byte_size = 2**64 - 32 - - if self.protocol == "http": - iu.shm_basic_infer( - self, - self.triton_client, - self._shm_handles[0], - self._shm_handles[1], - self._shm_handles[2], - self._shm_handles[3], - error_msg, - shm_output_offset=offset, - shm_output_byte_size=byte_size, - protocol=self.protocol, - use_system_shared_memory=True, - ) - - self.assertEqual(len(error_msg), 1) - self.assertTrue( - "Integer overflow detected: byte_size " in error_msg[0], - f"Unexpected error message: {error_msg[0]}", - ) - self._cleanup_shm_handles() - else: - # The gRPC client utilizes the int64_param and will throw a separate error for values larger than 2**63-1 - try: - iu.shm_basic_infer( - self, - self.triton_client, - self._shm_handles[0], - self._shm_handles[1], - self._shm_handles[2], - self._shm_handles[3], - error_msg, - shm_output_offset=offset, - shm_output_byte_size=byte_size, - protocol=self.protocol, - use_system_shared_memory=True, - ) - self.assertTrue( - False, - "Expected gRPC client to fail on value larger than int64_param maximum", - ) - except ValueError as ex: - self.assertIn("Value out of range:", str(ex)) - self._cleanup_shm_handles() - def test_register_out_of_bound(self): create_byte_size = self.DEFAULT_SHM_BYTE_SIZE @@ -598,53 +456,6 @@ def test_python_client_leak(self): "client memory usage is increasing", ) - def test_register_reserved_names(self): - """ - Test that registration fails if attempting to use a reserved - prefix for the shm key. - """ - # This matches kTritonSharedMemoryRegionPrefix in the server code. - reserved_prefix = "triton_python_backend_shm_region_" - shm_name = "my_test_shm_name" - - # The shared memory key cannot start with the reserved prefix, - # regardless of leading slashes. - shm_keys_to_test = [ - f"{reserved_prefix}_my_test_shm_key", - f"/{reserved_prefix}_my_test_shm_key", - f"///{reserved_prefix}_my_test_shm_key", - ] - - for shm_key in shm_keys_to_test: - with self.subTest(shm_key=shm_key): - expected_msg = f"cannot register shared memory region '{shm_name}' with key '{shm_key}' as the key contains the reserved prefix '{reserved_prefix}'" - with self.assertRaisesRegex( - utils.InferenceServerException, expected_msg - ): - self.triton_client.register_system_shared_memory( - shm_name, shm_key, 10000 - ) - - def test_register_invalid_shm_key(self): - """ - Test that registration fails if attempting to use an invalid name for the shm key. - """ - shm_name = "my_test_shm_name" - shm_keys_to_test = [ - "/", - "///", - ] - - for shm_key in shm_keys_to_test: - with self.subTest(shm_key=shm_key): - expected_msg = f"cannot register shared memory region '{shm_name}' - invalid shm key '{shm_key}'" - with self.assertRaisesRegex( - utils.InferenceServerException, expected_msg - ): - self.triton_client.register_system_shared_memory( - shm_name, shm_key, 10000 - ) - def callback(user_data, result, error): if error: diff --git a/qa/L0_shared_memory/test.sh b/qa/L0_shared_memory/test.sh index b8a02338fc..27e8b2dd80 100755 --- a/qa/L0_shared_memory/test.sh +++ b/qa/L0_shared_memory/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -25,78 +25,21 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -REPO_VERSION=${NVIDIA_TRITON_SERVER_VERSION} -if [ "$#" -ge 1 ]; then - REPO_VERSION=$1 -fi -if [ -z "$REPO_VERSION" ]; then - echo -e "Repository version must be specified" - echo -e "\n***\n*** Test Failed\n***" - exit 1 -fi -if [ ! -z "$TEST_REPO_ARCH" ]; then - REPO_VERSION=${REPO_VERSION}_${TEST_REPO_ARCH} -fi - CLIENT_LOG="./client.log" SHM_TEST=shared_memory_test.py TEST_RESULT_FILE='test_results.txt' # Configure to support test on jetson as well TRITON_DIR=${TRITON_DIR:="/opt/tritonserver"} -DATADIR=/data/inferenceserver/${REPO_VERSION} SERVER=${TRITON_DIR}/bin/tritonserver BACKEND_DIR=${TRITON_DIR}/backends +SERVER_ARGS_EXTRA="--backend-directory=${BACKEND_DIR}" source ../common/util.sh pip3 install psutil RET=0 rm -fr *.log -# Test that shared memory registration/unregistration is rejected and status -# query is allowed when --allow-client-shm is not set (default is false). -SERVER_ARGS_EXTRA="--backend-directory=${BACKEND_DIR}" -for client_type in http grpc; do - SERVER_ARGS="--model-repository=`pwd`/models --log-verbose=1 ${SERVER_ARGS_EXTRA}" - SERVER_LOG="./test_client_shm_disabled_by_default.$client_type.server.log" - run_server - if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 - fi - - export CLIENT_TYPE=$client_type - TMP_CLIENT_LOG="./tmp_client.log" - echo "Test: test_client_shm_disabled_by_default, client type: $client_type" >>$TMP_CLIENT_LOG - - set +e - python3 $SHM_TEST SharedMemoryTest.test_client_shm_disabled_by_default >>$TMP_CLIENT_LOG 2>&1 - if [ $? -ne 0 ]; then - cat $TMP_CLIENT_LOG - echo -e "\n***\n*** Test Failed\n***" - RET=1 - else - check_test_results $TEST_RESULT_FILE 1 - if [ $? -ne 0 ]; then - cat $TEST_RESULT_FILE - echo -e "\n***\n*** Test Result Verification Failed\n***" - RET=1 - fi - fi - cat $TMP_CLIENT_LOG >>$CLIENT_LOG - rm $TMP_CLIENT_LOG - kill $SERVER_PID - wait $SERVER_PID - if [ $? -ne 0 ]; then - echo -e "\n***\n*** Test Server shut down non-gracefully\n***" - RET=1 - fi - set -e -done - -# Test shared memory registration with --allow-client-shm=true -SERVER_ARGS_EXTRA="--backend-directory=${BACKEND_DIR} --allow-client-shm=true" for i in \ test_invalid_create_shm \ test_valid_create_set_register \ @@ -110,10 +53,7 @@ for i in \ test_unregisterall \ test_infer_offset_out_of_bound \ test_infer_byte_size_out_of_bound \ - test_infer_integer_overflow \ test_register_out_of_bound \ - test_register_reserved_names \ - test_register_invalid_shm_key \ test_python_client_leak; do for client_type in http grpc; do SERVER_ARGS="--model-repository=`pwd`/models --log-verbose=1 ${SERVER_ARGS_EXTRA}" @@ -200,60 +140,6 @@ for test_case in \ done done -# Test large system shared memory offset -rm -rf models/* -# prepare add_sub model of various backends -BACKENDS=${BACKENDS:-"python onnx libtorch plan openvino"} -for backend in ${BACKENDS} ; do - model="${backend}_int32_int32_int32" - model_dir="models/${model}" - if [[ $backend == "python" ]]; then - mkdir -p ${model_dir}/1 - cp ../python_models/add_sub/model.py ${model_dir}/1/ - cp ../python_models/add_sub/config.pbtxt ${model_dir}/ - sed -i 's/TYPE_FP32/TYPE_INT32/g' ${model_dir}/config.pbtxt - echo "max_batch_size: 8" >> ${model_dir}/config.pbtxt - else - mkdir -p ${model_dir} - cp -r $DATADIR/qa_model_repository/${model}/1 ${model_dir}/1 - cp $DATADIR/qa_model_repository/${model}/config.pbtxt ${model_dir}/ - cp $DATADIR/qa_model_repository/${model}/output0_labels.txt ${model_dir}/ - if [ $backend == "openvino" ]; then - echo 'parameters { key: "ENABLE_BATCH_PADDING" value { string_value: "YES" } }' >> models/${model}/config.pbtxt - fi - fi -done - -test_case="test_large_shm_register_offset" -for client_type in http grpc; do - SERVER_ARGS="--model-repository=`pwd`/models --log-verbose=1 ${SERVER_ARGS_EXTRA}" - SERVER_LOG="./${test_case}.${client_type}.server.log" - run_server - if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 - fi - - export CLIENT_TYPE=$client_type - CLIENT_LOG="./${test_case}.${client_type}.client.log" - set +e - python3 $SHM_TEST SharedMemoryTest.${test_case} >>"$CLIENT_LOG" 2>&1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Failed - ${client_type}\n***" - RET=1 - fi - - kill $SERVER_PID - wait $SERVER_PID - if [ $? -ne 0 ]; then - echo -e "\n***\n*** Test Server shut down non-gracefully\n***" - RET=1 - fi - set -e -done - if [ $RET -eq 0 ]; then echo -e "\n***\n*** Test Passed\n***" else diff --git a/qa/L0_simple_ensemble/backpressure_test_models/decoupled_producer/1/model.py b/qa/L0_simple_ensemble/backpressure_test_models/decoupled_producer/1/model.py deleted file mode 100644 index 9b2a0a0141..0000000000 --- a/qa/L0_simple_ensemble/backpressure_test_models/decoupled_producer/1/model.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -import time - -import numpy as np -import triton_python_backend_utils as pb_utils - - -class TritonPythonModel: - """ - Decoupled model that produces N responses based on input value. - """ - - def execute(self, requests): - for request in requests: - # Get input - number of responses to produce - in_tensor = pb_utils.get_input_tensor_by_name(request, "IN") - count = in_tensor.as_numpy().item() - - response_sender = request.get_response_sender() - out_tensor = pb_utils.Tensor("OUT", np.array([[0.5]], dtype=np.float32)) - - # Produce 'count' responses, each with 0.5 as the output value - for i in range(count): - time.sleep(0.1) # Simulate some processing delay - response = pb_utils.InferenceResponse(output_tensors=[out_tensor]) - response_sender.send(response) - - # Send final flag - response_sender.send(flags=pb_utils.TRITONSERVER_RESPONSE_COMPLETE_FINAL) - - return None diff --git a/qa/L0_simple_ensemble/backpressure_test_models/decoupled_producer/config.pbtxt b/qa/L0_simple_ensemble/backpressure_test_models/decoupled_producer/config.pbtxt deleted file mode 100644 index 0f7e05db0e..0000000000 --- a/qa/L0_simple_ensemble/backpressure_test_models/decoupled_producer/config.pbtxt +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -name: "decoupled_producer" -backend: "python" -max_batch_size: 1 - -input [ - { - name: "IN" - data_type: TYPE_INT32 - dims: [ 1 ] - } -] - -output [ - { - name: "OUT" - data_type: TYPE_FP32 - dims: [ 1 ] - } -] - -instance_group [ - { - count: 1 - kind: KIND_CPU - } -] - -model_transaction_policy { - decoupled: true -} diff --git a/qa/L0_simple_ensemble/backpressure_test_models/ensemble_disabled_max_inflight_requests/config.pbtxt b/qa/L0_simple_ensemble/backpressure_test_models/ensemble_disabled_max_inflight_requests/config.pbtxt deleted file mode 100644 index 804299cb21..0000000000 --- a/qa/L0_simple_ensemble/backpressure_test_models/ensemble_disabled_max_inflight_requests/config.pbtxt +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -platform: "ensemble" -max_batch_size: 0 - -input [ - { - name: "IN" - data_type: TYPE_INT32 - dims: [ 1 ] - } -] - -output [ - { - name: "OUT" - data_type: TYPE_FP32 - dims: [ 1 ] - } -] - -ensemble_scheduling { - step [ - { - model_name: "decoupled_producer" - model_version: -1 - input_map { - key: "IN" - value: "IN" - } - output_map { - key: "OUT" - value: "intermediate" - } - }, - { - model_name: "slow_consumer" - model_version: -1 - input_map { - key: "INPUT0" - value: "intermediate" - } - output_map { - key: "OUTPUT0" - value: "OUT" - } - } - ] -} - diff --git a/qa/L0_simple_ensemble/ensemble_backpressure_test.py b/qa/L0_simple_ensemble/ensemble_backpressure_test.py deleted file mode 100755 index e57b5b8cc0..0000000000 --- a/qa/L0_simple_ensemble/ensemble_backpressure_test.py +++ /dev/null @@ -1,554 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import sys - -sys.path.append("../common") - -import os -import queue -import threading -import time -import unittest -from contextlib import ExitStack -from functools import partial - -import numpy as np -import test_util as tu -import tritonclient.grpc as grpcclient -from tritonclient.utils import InferenceServerException - -SERVER_URL = "localhost:8001" -DEFAULT_RESPONSE_TIMEOUT = 60 -EXPECTED_INFER_OUTPUT = 0.5 -MODEL_ENSEMBLE_PARALLEL_FAILED_ENQUEUE = "ensemble_parallel_step_failed_enqueue" -EXPECTED_PARALLEL_FAILED_ENQUEUE_OUTPUT = 4.0 - -NUM_REQUESTS = 16 -NUM_RESPONSES_PER_REQUEST = 8 - - -class UserData: - def __init__(self): - self._response_queue = queue.Queue() - - -def callback(user_data, result, error): - if error: - user_data._response_queue.put(error) - else: - user_data._response_queue.put(result) - - -def prepare_infer_args(input_value, enable_batching=False): - """ - Create InferInput/InferRequestedOutput lists - """ - if enable_batching: - input_data = np.array([[input_value]], dtype=np.int32) - else: - input_data = np.array([input_value], dtype=np.int32) - infer_input = [grpcclient.InferInput("IN", input_data.shape, "INT32")] - infer_input[0].set_data_from_numpy(input_data) - outputs = [grpcclient.InferRequestedOutput("OUT")] - return infer_input, outputs - - -def collect_responses(user_data, timeout=DEFAULT_RESPONSE_TIMEOUT): - """ - Collect responses from user_data until the final response flag is seen. - """ - errors = [] - responses = [] - while True: - try: - result = user_data._response_queue.get(timeout=timeout) - except queue.Empty: - raise Exception(f"No response received within {timeout} seconds.") - - if isinstance(result, InferenceServerException): - errors.append(result) - # error responses are final - stream terminates - break - - response = result.get_response() - # Add response to list if it has data (not empty final-only response) - if len(response.outputs) > 0: - responses.append(result) - - # Check if this is the final response - final = response.parameters.get("triton_final_response") - if final and final.bool_param: - break - - return errors, responses - - -class EnsembleBackpressureTest(tu.TestResultCollector): - """ - Tests for ensemble backpressure feature (max_inflight_requests). - """ - - def _run_inference( - self, model_name, expected_responses_per_request, num_concurrent_requests=1 - ): - """ - Send num_concurrent_requests streaming requests to model_name, each expecting - expected_responses_per_request responses. Verify all complete with correct data. - """ - user_datas = [UserData() for _ in range(num_concurrent_requests)] - - with ExitStack() as stack: - clients = [ - stack.enter_context(grpcclient.InferenceServerClient(SERVER_URL)) - for _ in range(num_concurrent_requests) - ] - - inputs, outputs = prepare_infer_args(expected_responses_per_request, True) - - # Start all concurrent requests - for i in range(num_concurrent_requests): - clients[i].start_stream(callback=partial(callback, user_datas[i])) - clients[i].async_stream_infer( - model_name=model_name, inputs=inputs, outputs=outputs - ) - - # Collect and verify responses for all requests - for i, ud in enumerate(user_datas): - errors, responses = collect_responses(ud) - self.assertEqual( - len(responses), - expected_responses_per_request, - f"Request {i}: expected {expected_responses_per_request} responses, got {len(responses)}", - ) - self.assertEqual( - len(errors), 0, f"Request {i}: unexpected errors: {errors}" - ) - # Verify correctness of responses - for idx, resp in enumerate(responses): - output = resp.as_numpy("OUT") - # output shape is [batch_size, 1]; extract scalar for comparison. - value = float(output[0][0]) - self.assertAlmostEqual( - value, - EXPECTED_INFER_OUTPUT, - places=5, - msg=f"Request {i} response {idx}: expected " - f"{EXPECTED_INFER_OUTPUT}, got {value}", - ) - - # Stop all streams - for client in clients: - client.stop_stream() - - def test_single_request_with_different_limits(self): - """ - Single streaming request that produces 16 responses via a three-step ensemble pipeline - (decoupled_producer → consumer_high_delay → consumer_low_delay) under various - max_inflight_requests configurations. - """ - cases = [ - ("ensemble_limit_4", "max_inflight_requests=4"), - ("ensemble_limit_1", "max_inflight_requests=1"), - ("ensemble_disabled", "max_inflight_requests is disabled"), - ] - for model_name, desc in cases: - with self.subTest(limit=desc): - self._run_inference( - model_name=model_name, expected_responses_per_request=16 - ) - - def test_concurrent_requests_with_different_limits(self): - """ - NUM_REQUESTS concurrent streaming requests (NUM_RESPONSES_PER_REQUEST - responses each) exercise the max_inflight_requests limit. - Subtests cover: limit=4, limit=1, and the limit disabled. - """ - cases = [ - ("ensemble_limit_4", "max_inflight_requests=4"), - ("ensemble_limit_1", "max_inflight_requests=1"), - ("ensemble_disabled", "max_inflight_requests is disabled"), - ] - for model_name, desc in cases: - with self.subTest(limit=desc): - self._run_inference( - model_name=model_name, - expected_responses_per_request=NUM_RESPONSES_PER_REQUEST, - num_concurrent_requests=NUM_REQUESTS, - ) - - def test_sequential_requests_limiter_resets_cleanly(self): - """ - Send NUM_REQUESTS requests one after another. If the limiter - leaks a slot on any request, subsequent requests will be stuck or time out. - """ - for seq_idx in range(NUM_REQUESTS): - with self.subTest(request=seq_idx): - self._run_inference( - model_name="ensemble_limit_4", - expected_responses_per_request=NUM_RESPONSES_PER_REQUEST, - ) - - def test_request_cancellation_under_backpressure(self): - """ - Start a long-running request (32 responses), cancel mid-stream, - and verify the server sends a CANCELLED status and only a partial set of - responses is received. - """ - input_value = 32 - user_data = UserData() - - with grpcclient.InferenceServerClient(SERVER_URL) as triton_client: - inputs, outputs = prepare_infer_args(input_value, True) - triton_client.start_stream(callback=partial(callback, user_data)) - - # Start the request - triton_client.async_stream_infer( - model_name="ensemble_limit_4", inputs=inputs, outputs=outputs - ) - - responses = [] - try: - result = user_data._response_queue.get(timeout=5) - if isinstance(result, InferenceServerException): - self.fail(f"Got error before cancellation: {result}") - resp = result.get_response() - if len(resp.outputs) > 0: - responses.append(result) - except queue.Empty: - self.fail("Stream did not produce any response before cancellation.") - - # Cancel the stream - this unblocks any waiting producers and triggers a CANCELLED error. - triton_client.stop_stream(cancel_requests=True) - - # Allow some time for cancellation - time.sleep(1) - - cancellation_found = False - while True: - try: - result = user_data._response_queue.get(timeout=1) - if isinstance(result, InferenceServerException): - self.assertEqual( - result.status(), - "StatusCode.CANCELLED", - f"Expected CANCELLED status, got: {result.status()}", - ) - cancellation_found = True - break - else: - response = result.get_response() - if len(response.outputs) > 0: - responses.append(result) - # Check for final response - final = response.parameters.get("triton_final_response") - if final and final.bool_param: - break - except queue.Empty: - break - - # Verify the cancellation error was received - self.assertTrue( - cancellation_found, - "Did not receive the expected cancellation error from the server.", - ) - - # Verify we received only a partial set of responses - self.assertLess( - len(responses), - input_value, - "Expected partial responses due to cancellation, but received all of them.", - ) - self.assertGreater( - len(responses), - 0, - "Expected to receive at least one response before cancellation.", - ) - - -class EnsembleStepMaxQueueSizeTest(tu.TestResultCollector): - def _run_inference(self, model_name, expected_responses_count): - """ - Helper function for streaming inference. - - For decoupled streaming ensembles with queue limit on internal step: - - Each producer response creates an independent flow through the ensemble - - Flows that complete before error is set send their outputs successfully - - Once error occurs (queue full), stream terminates with error - - Result: 0-N successful responses + 1 error (N depends on timing) - """ - user_data = UserData() - with grpcclient.InferenceServerClient(SERVER_URL) as triton_client: - try: - inputs, outputs = prepare_infer_args(expected_responses_count) - triton_client.start_stream(callback=partial(callback, user_data)) - triton_client.async_stream_infer( - model_name=model_name, inputs=inputs, outputs=outputs - ) - - # Collect and verify responses - errors, responses = collect_responses(user_data) - self.assertGreaterEqual( - len(responses), - 0, - "May have 0 or more successful responses depending on timing", - ) - self.assertLess( - len(responses), - expected_responses_count, - f"Should have fewer than {expected_responses_count} responses (some flows failed)", - ) - self.assertEqual( - len(errors), - 1, - "Expected exactly one error when the queue is full and the stream terminates", - ) - - # Verify correctness of successful responses - for idx, resp in enumerate(responses): - output = resp.as_numpy("OUT") - self.assertAlmostEqual( - output[0], - EXPECTED_INFER_OUTPUT, - places=5, - msg=f"Response {idx} has incorrect value - {output[0]}", - ) - - # Verify error is queue-full error - self.assertIn( - "Exceeds maximum queue size", - str(errors[0]), - f"Expected queue size error, got: {str(errors[0])}", - ) - finally: - triton_client.stop_stream() - - def _run_concurrent_inference(self, model_name, expected_responses_count): - """ - Helper function for concurrent independent requests. - Each request either succeeds completely or fails completely. - Returns: (num_successes, num_errors) tuple - """ - user_data = UserData() - with grpcclient.InferenceServerClient(SERVER_URL) as triton_client: - try: - inputs, outputs = prepare_infer_args(expected_responses_count) - triton_client.start_stream(callback=partial(callback, user_data)) - triton_client.async_stream_infer( - model_name=model_name, inputs=inputs, outputs=outputs - ) - - # Collect responses - errors, responses = collect_responses(user_data) - - # For concurrent independent requests with queue limit on internal step: - # - Requests that arrive before queue fills: succeed with all outputs - # - Requests that arrive after queue fills: fail with error - total = len(responses) + len(errors) - self.assertEqual( - total, - expected_responses_count, - f"Expected {expected_responses_count} total responses, got {total}", - ) - - if len(errors) > 0: - # This request failed - self.assertEqual( - len(responses), - 0, - "Failed request should have no successful outputs", - ) - self.assertEqual( - len(errors), 1, "Failed request should have exactly one error" - ) - self.assertIn( - "Exceeds maximum queue size", - str(errors[0]), - f"Expected queue size error, got: {str(errors[0])}", - ) - return (0, 1) # 0 successes, 1 error - else: - # This request succeeded - self.assertEqual( - len(responses), - expected_responses_count, - f"Successful request should have all {expected_responses_count} outputs", - ) - # Verify correctness of successful responses - for idx, resp in enumerate(responses): - output = resp.as_numpy("OUT") - self.assertAlmostEqual( - output[0], - EXPECTED_INFER_OUTPUT, - places=5, - msg=f"Response {idx} has incorrect value - {output[0]}", - ) - return (expected_responses_count, 0) # N successes, 0 errors - finally: - triton_client.stop_stream() - - def test_step1_max_queue_size(self): - """ - Test max_queue_size on step 1 (decoupled_producer). - - Trigger 32 concurrent ensemble requests, each producing 1 response - - Step 1 (producer) has max_queue_size limit - - Some ensemble requests succeed completely (before queue fills) - - Some fail completely (when producer queue is full) - """ - model_name = "ensemble_step1_enabled_max_queue_size" - num_requests = 32 - - # Store results from each thread - results = [] - - def thread_wrapper(model_name, expected_count, results_list): - """Wrapper to capture thread results""" - result = self._run_concurrent_inference(model_name, expected_count) - results_list.append(result) - - # Launch concurrent threads to perform infer requests - threads = [] - for i in range(num_requests): - t = threading.Thread(target=thread_wrapper, args=(model_name, 1, results)) - threads.append(t) - t.start() - - # Wait for all requests to complete - for t in threads: - t.join(timeout=60) - - # Aggregate results from all threads - total_successes = sum(r[0] for r in results) - total_errors = sum(r[1] for r in results) - - # Verify aggregate behavior - self.assertEqual( - total_successes + total_errors, - num_requests, - f"Expected {num_requests} total results (successes + errors), " - f"got {total_successes} successes + {total_errors} errors = {total_successes + total_errors}", - ) - - # Verify at least some errors occurred (queue limit was hit) - self.assertGreater( - total_errors, - 0, - f"Expected some errors due to max_queue_size limit, " - f"but all {num_requests} requests succeeded.", - ) - - # Verify at least some successes occurred (not all rejected) - self.assertGreater( - total_successes, - 0, - f"Expected some successful requests before queue filled, " - f"but all {num_requests} requests failed.", - ) - - def test_step2_max_queue_size(self): - """ - Test max_queue_size on step 2 (slow_consumer). - - Trigger 1 streaming ensemble request producing 32 responses - - Step 1 (producer) generates 32 responses rapidly (every 100ms) - - Step 2 (consumer) has max_queue_size=5 and processes slowly (500ms each) - - Each producer response is an independent request to the second step through - - the ensemble flow. Some requests complete successfully before queue fills - - When queue fills, error is set and stream terminates - - All inflight steps drain, then error response sent to client - """ - model_name = "ensemble_step2_enabled_max_queue_size" - self._run_inference(model_name=model_name, expected_responses_count=32) - - -class EnsembleParallelFailedEnqueueTest(tu.TestResultCollector): - def _run_inference(self, expected_responses_count=32): - """ - Exercise a fan-out ensemble where one parallel branch hits queue-full - first. Successful responses emitted before the failure should still be - correct, and the stream should terminate with exactly one queue-full - error. - """ - user_data = UserData() - with grpcclient.InferenceServerClient(SERVER_URL) as triton_client: - try: - inputs, outputs = prepare_infer_args(expected_responses_count) - triton_client.start_stream(callback=partial(callback, user_data)) - triton_client.async_stream_infer( - model_name=MODEL_ENSEMBLE_PARALLEL_FAILED_ENQUEUE, - inputs=inputs, - outputs=outputs, - ) - - errors, responses = collect_responses(user_data, timeout=15) - self.assertLess( - len(responses), - expected_responses_count, - "Expected the parallel slow branch to queue-fill before all " - "responses completed.", - ) - self.assertEqual( - len(errors), - 1, - "Expected exactly one queue-full error from the parallel " - "failed-enqueue path.", - ) - self.assertIn( - "Exceeds maximum queue size", - str(errors[0]), - f"Expected queue size error, got: {str(errors[0])}", - ) - - for idx, resp in enumerate(responses): - output = resp.as_numpy("OUT") - self.assertAlmostEqual( - float(np.squeeze(output)), - EXPECTED_PARALLEL_FAILED_ENQUEUE_OUTPUT, - places=5, - msg=f"Response {idx} has incorrect value - {output}", - ) - finally: - triton_client.stop_stream() - - def test_parallel_step_failed_enqueue(self): - """ - Repeat the same request according to PARALLEL_FAILED_ENQUEUE_LOOPS. - """ - loop_count = int(os.environ.get("PARALLEL_FAILED_ENQUEUE_LOOPS", "1")) - self.assertGreaterEqual( - loop_count, 1, "PARALLEL_FAILED_ENQUEUE_LOOPS must be >= 1" - ) - - for iteration in range(loop_count): - with self.subTest(iteration=iteration): - self._run_inference() - - -if __name__ == "__main__": - unittest.main() diff --git a/qa/L0_simple_ensemble/test.sh b/qa/L0_simple_ensemble/test.sh index 1e62c91e7b..927f36ea32 100755 --- a/qa/L0_simple_ensemble/test.sh +++ b/qa/L0_simple_ensemble/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2026, NVIDIA CORPORATION. All rights reserved. +# Copyright 2019-2024, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -31,17 +31,15 @@ SIMPLE_TEST_PY=./ensemble_test.py CLIENT_LOG="./client.log" -TEST_MODEL_DIR="`pwd`/models" -BACKPRESSURE_TEST_MODEL_DIR="`pwd`/backpressure_test_models" TEST_RESULT_FILE='test_results.txt' SERVER=/opt/tritonserver/bin/tritonserver -SERVER_ARGS="--model-repository=${TEST_MODEL_DIR}" +SERVER_ARGS="--model-repository=`pwd`/models" SERVER_LOG="./inference_server.log" source ../common/util.sh # ensure ensemble models have version sub-directory -mkdir -p ${TEST_MODEL_DIR}/ensemble_add_sub_int32_int32_int32/1 -mkdir -p ${TEST_MODEL_DIR}/ensemble_partial_add_sub/1 +mkdir -p `pwd`/models/ensemble_add_sub_int32_int32_int32/1 +mkdir -p `pwd`/models/ensemble_partial_add_sub/1 rm -f $CLIENT_LOG $SERVER_LOG @@ -148,475 +146,10 @@ set -e kill $SERVER_PID wait $SERVER_PID -######## Test max_queue_size dynamic batching parameter in ensemble steps ######## -## Ensemble model: step1-decoupled_producer -> step2-slow_consumer -MAX_QUEUE_SIZE_TEST_MODEL_DIR="`pwd`/max_queue_size_test_models" -rm -rf ${MAX_QUEUE_SIZE_TEST_MODEL_DIR} - -# Enable max_queue_size in the first step (decoupled_producer) -mkdir -p ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/ensemble_step1_enabled_max_queue_size/1 \ - ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/decoupled_producer_enabled_max_queue_size/1 \ - ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/slow_consumer/1 -cp ${BACKPRESSURE_TEST_MODEL_DIR}/ensemble_disabled_max_inflight_requests/config.pbtxt \ - ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/ensemble_step1_enabled_max_queue_size/ -sed -i 's/"decoupled_producer"/"decoupled_producer_enabled_max_queue_size"/g' \ - ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/ensemble_step1_enabled_max_queue_size/config.pbtxt - -cp ../python_models/ground_truth/model.py ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/slow_consumer/1 -cp ../python_models/ground_truth/config.pbtxt ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/slow_consumer/ -sed -i 's/name: "ground_truth"/name: "slow_consumer"/g' ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/slow_consumer/config.pbtxt -sed -i 's/max_batch_size: 64/max_batch_size: 1/g' ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/slow_consumer/config.pbtxt - -cp ${BACKPRESSURE_TEST_MODEL_DIR}/decoupled_producer/1/model.py ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/decoupled_producer_enabled_max_queue_size/1 -cp ${BACKPRESSURE_TEST_MODEL_DIR}/decoupled_producer/config.pbtxt ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/decoupled_producer_enabled_max_queue_size/ -sed -i 's/name: "decoupled_producer"/name: "decoupled_producer_enabled_max_queue_size"/g' ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/decoupled_producer_enabled_max_queue_size/config.pbtxt -# Add dynamic_batching with max_queue_size to decoupled_producer -cat >> ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/decoupled_producer_enabled_max_queue_size/config.pbtxt << 'EOF' - -dynamic_batching { - preferred_batch_size: [ 1 ] - default_queue_policy { - max_queue_size: 4 - } -} -EOF - -# Enable max_queue_size in the second step (slow_consumer) -mkdir -p ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/ensemble_step2_enabled_max_queue_size/1 \ - ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/decoupled_producer/1 ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/slow_consumer_enabled_max_queue_size/1 -cp ${BACKPRESSURE_TEST_MODEL_DIR}/ensemble_disabled_max_inflight_requests/config.pbtxt \ - ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/ensemble_step2_enabled_max_queue_size/ -sed -i 's/"slow_consumer"/"slow_consumer_enabled_max_queue_size"/g' \ - ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/ensemble_step2_enabled_max_queue_size/config.pbtxt - -cp ${BACKPRESSURE_TEST_MODEL_DIR}/decoupled_producer/1/model.py ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/decoupled_producer/1 -cp ${BACKPRESSURE_TEST_MODEL_DIR}/decoupled_producer/config.pbtxt ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/decoupled_producer/ - -cp ../python_models/ground_truth/model.py ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/slow_consumer_enabled_max_queue_size/1 -cp ../python_models/ground_truth/config.pbtxt ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/slow_consumer_enabled_max_queue_size/ -sed -i 's/name: "ground_truth"/name: "slow_consumer_enabled_max_queue_size"/g' \ - ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/slow_consumer_enabled_max_queue_size/config.pbtxt -sed -i 's/max_batch_size: 64/max_batch_size: 1/g' ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/slow_consumer_enabled_max_queue_size/config.pbtxt -# Add dynamic_batching with max_queue_size to slow_consumer -cat >> ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/slow_consumer_enabled_max_queue_size/config.pbtxt << 'EOF' - -dynamic_batching { - preferred_batch_size: [ 1 ] - default_queue_policy { - max_queue_size: 4 - } -} -EOF - -BACKPRESSURE_TEST_PY=./ensemble_backpressure_test.py -TEST_NAME="EnsembleStepMaxQueueSizeTest" -SERVER_LOG="./ensemble_step_max_queue_size_test_server.log" -CLIENT_LOG="./ensemble_step_max_queue_size_test_client.log" -rm -f $SERVER_LOG $CLIENT_LOG - -SERVER_ARGS="--model-repository=${MAX_QUEUE_SIZE_TEST_MODEL_DIR}" -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 -fi - -set +e -python $BACKPRESSURE_TEST_PY $TEST_NAME -v >> $CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - RET=1 - cat $CLIENT_LOG -else - check_test_results $TEST_RESULT_FILE 2 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Result Verification Failed\n***" - RET=1 - fi -fi -set -e - -kill $SERVER_PID -wait $SERVER_PID - - -######## Test parallel-step failed enqueue path in ensemble scheduler ######## -PARALLEL_FAILED_ENQUEUE_MODEL_DIR="`pwd`/parallel_failed_enqueue_test_models" -rm -rf ${PARALLEL_FAILED_ENQUEUE_MODEL_DIR} - -mkdir -p ${PARALLEL_FAILED_ENQUEUE_MODEL_DIR}/ensemble_parallel_step_failed_enqueue/1 -mkdir -p ${PARALLEL_FAILED_ENQUEUE_MODEL_DIR}/decoupled_producer_parallel_queue/1 -mkdir -p ${PARALLEL_FAILED_ENQUEUE_MODEL_DIR}/slow_consumer_queue_limited/1 -mkdir -p ${PARALLEL_FAILED_ENQUEUE_MODEL_DIR}/fast_consumer/1 -mkdir -p ${PARALLEL_FAILED_ENQUEUE_MODEL_DIR}/join_add_sub/1 - -# Producer emits repeated responses with a larger payload value so the -# queue-limited branch fills first. -cp ${BACKPRESSURE_TEST_MODEL_DIR}/decoupled_producer/1/model.py \ - ${PARALLEL_FAILED_ENQUEUE_MODEL_DIR}/decoupled_producer_parallel_queue/1 -cp ${BACKPRESSURE_TEST_MODEL_DIR}/decoupled_producer/config.pbtxt \ - ${PARALLEL_FAILED_ENQUEUE_MODEL_DIR}/decoupled_producer_parallel_queue/ -sed -i 's/name: "decoupled_producer"/name: "decoupled_producer_parallel_queue"/g' \ - ${PARALLEL_FAILED_ENQUEUE_MODEL_DIR}/decoupled_producer_parallel_queue/config.pbtxt -sed -i 's/0.5/2.0/g' \ - ${PARALLEL_FAILED_ENQUEUE_MODEL_DIR}/decoupled_producer_parallel_queue/1/model.py - -# Queue-limited branch used to trigger a failed enqueue. -cp ../python_models/ground_truth/model.py \ - ${PARALLEL_FAILED_ENQUEUE_MODEL_DIR}/slow_consumer_queue_limited/1 -cp ../python_models/ground_truth/config.pbtxt \ - ${PARALLEL_FAILED_ENQUEUE_MODEL_DIR}/slow_consumer_queue_limited/ -sed -i 's/name: "ground_truth"/name: "slow_consumer_queue_limited"/g' \ - ${PARALLEL_FAILED_ENQUEUE_MODEL_DIR}/slow_consumer_queue_limited/config.pbtxt -sed -i 's/max_batch_size: 64/max_batch_size: 1/g' \ - ${PARALLEL_FAILED_ENQUEUE_MODEL_DIR}/slow_consumer_queue_limited/config.pbtxt -cat >> ${PARALLEL_FAILED_ENQUEUE_MODEL_DIR}/slow_consumer_queue_limited/config.pbtxt << 'EOF' - -dynamic_batching { - preferred_batch_size: [ 1 ] - default_queue_policy { - max_queue_size: 1 - } -} -EOF - -# Parallel branch with the same interface and no added delay. -cp ../python_models/ground_truth/model.py ${PARALLEL_FAILED_ENQUEUE_MODEL_DIR}/fast_consumer/1 -cp ../python_models/ground_truth/config.pbtxt ${PARALLEL_FAILED_ENQUEUE_MODEL_DIR}/fast_consumer/ -sed -i 's/name: "ground_truth"/name: "fast_consumer"/g' \ - ${PARALLEL_FAILED_ENQUEUE_MODEL_DIR}/fast_consumer/config.pbtxt -sed -i 's/max_batch_size: 64/max_batch_size: 1/g' \ - ${PARALLEL_FAILED_ENQUEUE_MODEL_DIR}/fast_consumer/config.pbtxt -sed -i 's/time.sleep(delay)/time.sleep(0)/g' \ - ${PARALLEL_FAILED_ENQUEUE_MODEL_DIR}/fast_consumer/1/model.py - -# Join both parallel branches into the ensemble output. -cp ../python_models/join_add_sub/model.py ${PARALLEL_FAILED_ENQUEUE_MODEL_DIR}/join_add_sub/1 -cp ../python_models/join_add_sub/config.pbtxt ${PARALLEL_FAILED_ENQUEUE_MODEL_DIR}/join_add_sub/ - -cat > ${PARALLEL_FAILED_ENQUEUE_MODEL_DIR}/ensemble_parallel_step_failed_enqueue/config.pbtxt << 'EOF' -name: "ensemble_parallel_step_failed_enqueue" -platform: "ensemble" -max_batch_size: 0 - -input [ - { - name: "IN" - data_type: TYPE_INT32 - dims: [ 1 ] - } -] - -output [ - { - name: "OUT" - data_type: TYPE_FP32 - dims: [ 1 ] - } -] - -ensemble_scheduling { - step [ - { - model_name: "decoupled_producer_parallel_queue" - model_version: -1 - input_map { - key: "IN" - value: "IN" - } - output_map { - key: "OUT" - value: "intermediate" - } - }, - { - model_name: "slow_consumer_queue_limited" - model_version: -1 - input_map { - key: "INPUT0" - value: "intermediate" - } - output_map { - key: "OUTPUT0" - value: "slow_out" - } - }, - { - model_name: "fast_consumer" - model_version: -1 - input_map { - key: "INPUT0" - value: "intermediate" - } - output_map { - key: "OUTPUT0" - value: "fast_out" - } - }, - { - model_name: "join_add_sub" - model_version: -1 - input_map { - key: "INPUT0" - value: "slow_out" - } - input_map { - key: "INPUT1" - value: "fast_out" - } - output_map { - key: "OUTPUT0" - value: "OUT" - } - } - ] -} -EOF - -BACKPRESSURE_TEST_PY=./ensemble_backpressure_test.py -TEST_NAME="EnsembleParallelFailedEnqueueTest.test_parallel_step_failed_enqueue" -SERVER_LOG="./ensemble_parallel_failed_enqueue_test_server.log" -CLIENT_LOG="./ensemble_parallel_failed_enqueue_test_client.log" -rm -f $SERVER_LOG $CLIENT_LOG - -SERVER_ARGS="--model-repository=${PARALLEL_FAILED_ENQUEUE_MODEL_DIR}" -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 -fi - -set +e -PARALLEL_FAILED_ENQUEUE_LOOPS=${PARALLEL_FAILED_ENQUEUE_LOOPS:-1} \ -python $BACKPRESSURE_TEST_PY $TEST_NAME -v >> $CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - RET=1 - cat $CLIENT_LOG -else - check_test_results $TEST_RESULT_FILE 1 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Result Verification Failed\n***" - RET=1 - fi -fi - -if ! kill -0 $SERVER_PID > /dev/null 2>&1; then - cat $SERVER_LOG - echo -e "\n***\n*** Server exited during parallel failed enqueue test\n***" - RET=1 -else - wait_for_server_live $SERVER_PID 5 - if [ "$WAIT_RET" != "0" ]; then - cat $SERVER_LOG - echo -e "\n***\n*** Server did not remain live after parallel failed enqueue test\n***" - RET=1 - fi -fi -set -e - -kill $SERVER_PID > /dev/null 2>&1 || true -wait $SERVER_PID > /dev/null 2>&1 || true - - -######## Test backpressure feature - 'max_inflight_requests' config option ######## -ENSEMBLE_BACKPRESSURE_TEST_MODEL_DIR="`pwd`/ensemble_backpressure_test_models" -rm -rf ${ENSEMBLE_BACKPRESSURE_TEST_MODEL_DIR} - -TEST_NAME="EnsembleBackpressureTest" -SERVER_LOG="./ensemble_backpressure_test_server.log" -CLIENT_LOG="./ensemble_backpressure_test_client.log" -SERVER_ARGS="--model-repository=${ENSEMBLE_BACKPRESSURE_TEST_MODEL_DIR}" -rm -f $SERVER_LOG $CLIENT_LOG - -# Step 1 - decoupled_producer (batch size 2) -mkdir -p ${ENSEMBLE_BACKPRESSURE_TEST_MODEL_DIR}/decoupled_producer/1 -cp ${BACKPRESSURE_TEST_MODEL_DIR}/decoupled_producer/1/model.py ${ENSEMBLE_BACKPRESSURE_TEST_MODEL_DIR}/decoupled_producer/1/ -cp ${BACKPRESSURE_TEST_MODEL_DIR}/decoupled_producer/config.pbtxt ${ENSEMBLE_BACKPRESSURE_TEST_MODEL_DIR}/decoupled_producer/ -sed -i 's/max_batch_size: 1/max_batch_size: 2/g' ${ENSEMBLE_BACKPRESSURE_TEST_MODEL_DIR}/decoupled_producer/config.pbtxt - -generate_consumer_model() { - local name=$1 - local delay=$2 - - mkdir -p ${ENSEMBLE_BACKPRESSURE_TEST_MODEL_DIR}/${name}/1 - cat > ${ENSEMBLE_BACKPRESSURE_TEST_MODEL_DIR}/${name}/1/model.py << EOF -import time -import triton_python_backend_utils as pb_utils - -class TritonPythonModel: - def execute(self, requests): - responses = [] - for request in requests: - in_tensor = pb_utils.get_input_tensor_by_name(request, "INPUT0") - out_tensor = pb_utils.Tensor("OUTPUT0", in_tensor.as_numpy()) - responses.append(pb_utils.InferenceResponse([out_tensor])) - time.sleep(${delay}) - return responses -EOF - cat > ${ENSEMBLE_BACKPRESSURE_TEST_MODEL_DIR}/${name}/config.pbtxt << EOF -name: "${name}" -backend: "python" -max_batch_size: 2 -input [ { name: "INPUT0", data_type: TYPE_FP32, dims: [ 1 ] } ] -output [ { name: "OUTPUT0", data_type: TYPE_FP32, dims: [ 1 ] } ] -instance_group [ { count: 1, kind: KIND_CPU } ] -dynamic_batching { preferred_batch_size: [ 2 ] } -EOF -} - -generate_ensemble_model() { - local name=$1 - local limit=$2 - local batch_size=2 - - local limit_str="" - if [ "$limit" != "disabled" ]; then - limit_str="max_inflight_requests: $limit" - fi - - mkdir -p ${ENSEMBLE_BACKPRESSURE_TEST_MODEL_DIR}/${name}/1 - cat > ${ENSEMBLE_BACKPRESSURE_TEST_MODEL_DIR}/${name}/config.pbtxt << EOF -name: "${name}" -platform: "ensemble" -max_batch_size: ${batch_size} -input [ { name: "IN", data_type: TYPE_INT32, dims: [ 1 ] } ] -output [ { name: "OUT", data_type: TYPE_FP32, dims: [ 1 ] } ] -ensemble_scheduling { - ${limit_str} - step [ - { - model_name: "decoupled_producer" - model_version: -1 - input_map { key: "IN", value: "IN" } - output_map { key: "OUT", value: "intermediate_1" } - }, - { - model_name: "consumer_high_delay" - model_version: -1 - input_map { key: "INPUT0", value: "intermediate_1" } - output_map { key: "OUTPUT0", value: "intermediate_2" } - }, - { - model_name: "consumer_low_delay" - model_version: -1 - input_map { key: "INPUT0", value: "intermediate_2" } - output_map { key: "OUTPUT0", value: "OUT" } - } - ] -} -EOF -} - -# Steps 2 and 3 - consumer_high_delay and consumer_low_delay (batch size 2) -generate_consumer_model "consumer_high_delay" "0.5" -generate_consumer_model "consumer_low_delay" "0.1" - -# Ensemble models with different max_inflight_requests limits (including disabled) -generate_ensemble_model "ensemble_disabled" "disabled" -generate_ensemble_model "ensemble_limit_1" 1 -generate_ensemble_model "ensemble_limit_4" 4 - -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 -fi - -set +e - -# Verify valid config was loaded successfully -if ! grep -q "Ensemble model 'ensemble_limit_1' configured with max_inflight_requests: 1" $SERVER_LOG; then - echo -e "\n***\n*** FAILED: ensemble_limit_1 did not load\n***" - RET=1 -fi -if ! grep -q "Ensemble model 'ensemble_limit_4' configured with max_inflight_requests: 4" $SERVER_LOG; then - echo -e "\n***\n*** FAILED: ensemble_limit_4 did not load\n***" - RET=1 -fi - -python $BACKPRESSURE_TEST_PY $TEST_NAME -v >> $CLIENT_LOG 2>&1 -if [ $? -ne 0 ]; then - RET=1 - cat $CLIENT_LOG -else - check_test_results $TEST_RESULT_FILE 4 - if [ $? -ne 0 ]; then - cat $CLIENT_LOG - echo -e "\n***\n*** Test Result Verification Failed\n***" - RET=1 - fi -fi -set -e - -kill $SERVER_PID -wait $SERVER_PID - - -######## Test invalid values for 'max_inflight_requests' config option ######## -INVALID_PARAM_MODEL_DIR="`pwd`/invalid_param_test_models" -SERVER_ARGS="--model-repository=${INVALID_PARAM_MODEL_DIR}" -SERVER_LOG="./invalid_max_inflight_requests_server.log" -rm -rf $SERVER_LOG ${INVALID_PARAM_MODEL_DIR} - -mkdir -p ${INVALID_PARAM_MODEL_DIR}/ensemble_invalid_negative_limit/1 -mkdir -p ${INVALID_PARAM_MODEL_DIR}/ensemble_invalid_string_limit/1 -mkdir -p ${INVALID_PARAM_MODEL_DIR}/ensemble_invalid_large_value_limit/1 -# Reuse the decoupled_producer and slow_consumer models built in the previous test section. -cp -r ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/decoupled_producer ${MAX_QUEUE_SIZE_TEST_MODEL_DIR}/slow_consumer ${INVALID_PARAM_MODEL_DIR}/ - -# max_inflight_requests = -5 -cp ${BACKPRESSURE_TEST_MODEL_DIR}/ensemble_disabled_max_inflight_requests/config.pbtxt ${INVALID_PARAM_MODEL_DIR}/ensemble_invalid_negative_limit/ -sed -i 's/ensemble_scheduling {/ensemble_scheduling {\n max_inflight_requests: -5/g' \ - ${INVALID_PARAM_MODEL_DIR}/ensemble_invalid_negative_limit/config.pbtxt - -# max_inflight_requests = "invalid_value" -cp ${BACKPRESSURE_TEST_MODEL_DIR}/ensemble_disabled_max_inflight_requests/config.pbtxt ${INVALID_PARAM_MODEL_DIR}/ensemble_invalid_string_limit/ -sed -i 's/ensemble_scheduling {/ensemble_scheduling {\n max_inflight_requests: "invalid_value"/g' \ - ${INVALID_PARAM_MODEL_DIR}/ensemble_invalid_string_limit/config.pbtxt - -# max_inflight_requests = 12345678901 -cp ${BACKPRESSURE_TEST_MODEL_DIR}/ensemble_disabled_max_inflight_requests/config.pbtxt ${INVALID_PARAM_MODEL_DIR}/ensemble_invalid_large_value_limit/ -sed -i 's/ensemble_scheduling {/ensemble_scheduling {\n max_inflight_requests: 12345678901/g' \ - ${INVALID_PARAM_MODEL_DIR}/ensemble_invalid_large_value_limit/config.pbtxt - - -run_server -if [ "$SERVER_PID" != "0" ]; then - echo -e "\n***\n*** FAILED: unexpected success starting $SERVER\n***" - kill $SERVER_PID - wait $SERVER_PID - cat $SERVER_LOG - RET=1 -fi - -set +e -# Verify negative value caused model load failure -if ! grep -q "Expected integer, got: -" $SERVER_LOG; then - echo -e "\n***\n*** FAILED: Negative value should fail model load\n***" - RET=1 -fi - -# Verify invalid string caused model load failure -if ! grep -q 'Expected integer, got: "invalid_value"' $SERVER_LOG; then - echo -e "\n***\n*** FAILED: Invalid string should fail model load\n***" - RET=1 -fi - -# Verify very large value caused model load failure -if ! grep -q "Integer out of range (12345678901)" $SERVER_LOG; then - echo -e "\n***\n*** FAILED: Large value should fail model load\n***" - RET=1 -fi -set -e - - if [ $RET -eq 0 ]; then echo -e "\n***\n*** Test Passed\n***" else + cat $CLIENT_LOG echo -e "\n***\n*** Test FAILED\n***" fi diff --git a/qa/L0_storage_azure/test.sh b/qa/L0_storage_azure/test.sh index e2d9de760c..805c081679 100755 --- a/qa/L0_storage_azure/test.sh +++ b/qa/L0_storage_azure/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2020-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -137,10 +137,7 @@ sleep 10 # Test 1 Scenarios: # 1. access blob using shared key in envs -# 2. access blob using system-assigned managed identity -# 3. access blob using user-assigned managed identity -# 4. access blob using DefaultAzureCredential -# 5. adding more scenarios in future +# 2. adding more scenarios in future for ENV_VAR in "shared_key"; do SERVER_LOG=$SERVER_LOG_BASE.$ENV_VAR.log CLIENT_LOG=$CLIENT_LOG_BASE.$ENV_VAR.log @@ -172,117 +169,6 @@ for ENV_VAR in "shared_key"; do wait $SERVER_PID done -# Test 2: Managed Identity authentication -# Requires the test host (VM/AKS) to have a system-assigned managed identity -# with Storage Blob Data Reader on the test storage account. -# Skip if not running in an MI-capable environment. -if [ ! -z "$TEST_AZURE_MANAGED_IDENTITY" ]; then - echo -e "\n***\n*** Testing system-assigned Managed Identity\n***" - - # Save original key and clear it so it won't be used - SAVED_AZURE_STORAGE_KEY=$AZURE_STORAGE_KEY - unset AZURE_STORAGE_KEY - export AZURE_STORAGE_AUTH_TYPE="managed_identity" - - SERVER_LOG=$SERVER_LOG_BASE.managed_identity_system.log - CLIENT_LOG=$CLIENT_LOG_BASE.managed_identity_system.log - MODEL_REPO="${AS_URL}/models" - SERVER_ARGS="--model-repository=$MODEL_REPO --exit-timeout-secs=120" - - run_server - if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER with system-assigned MI\n***" - cat $SERVER_LOG - RET=1 - else - set +e - run_unit_tests - set -e - - kill $SERVER_PID - wait $SERVER_PID - fi - - # Test 3: User-assigned Managed Identity (if client ID is provided) - if [ ! -z "$AZURE_STORAGE_CLIENT_ID" ]; then - echo -e "\n***\n*** Testing user-assigned Managed Identity\n***" - - SERVER_LOG=$SERVER_LOG_BASE.managed_identity_user.log - CLIENT_LOG=$CLIENT_LOG_BASE.managed_identity_user.log - SERVER_ARGS="--model-repository=$MODEL_REPO --exit-timeout-secs=120" - - run_server - if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER with user-assigned MI\n***" - cat $SERVER_LOG - RET=1 - else - set +e - run_unit_tests - set -e - - kill $SERVER_PID - wait $SERVER_PID - fi - else - echo -e "\n***\n*** Skipping user-assigned MI test (AZURE_STORAGE_CLIENT_ID not set)\n***" - fi - - # Test 4: DefaultAzureCredential chain - echo -e "\n***\n*** Testing DefaultAzureCredential\n***" - export AZURE_STORAGE_AUTH_TYPE="default" - unset AZURE_STORAGE_CLIENT_ID - - SERVER_LOG=$SERVER_LOG_BASE.default_credential.log - CLIENT_LOG=$CLIENT_LOG_BASE.default_credential.log - SERVER_ARGS="--model-repository=$MODEL_REPO --exit-timeout-secs=120" - - run_server - if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER with DefaultAzureCredential\n***" - cat $SERVER_LOG - RET=1 - else - set +e - run_unit_tests - set -e - - kill $SERVER_PID - wait $SERVER_PID - fi - - # Test: invalid auth_type should fail gracefully - echo -e "\n***\n*** Testing invalid auth_type (expect failure)\n***" - export AZURE_STORAGE_AUTH_TYPE="invalid_type" - - SERVER_LOG=$SERVER_LOG_BASE.invalid_auth_type.log - SERVER_ARGS="--model-repository=$MODEL_REPO --exit-timeout-secs=120 --exit-on-error=false" - - run_server - if [ "$SERVER_PID" != "0" ]; then - # Server started — but model load should have failed. Verify the log - # contains an authentication error rather than a successful load. - if grep -q "Unable to create Azure filesystem client" $SERVER_LOG; then - echo -e "*** invalid auth_type correctly rejected ***" - else - echo -e "\n***\n*** Expected auth failure with invalid auth_type\n***" - cat $SERVER_LOG - RET=1 - fi - kill $SERVER_PID - wait $SERVER_PID - else - echo -e "*** Server correctly refused to start with invalid auth_type ***" - fi - - # Restore environment for remaining tests - unset AZURE_STORAGE_AUTH_TYPE - unset AZURE_STORAGE_CLIENT_ID - export AZURE_STORAGE_KEY=$SAVED_AZURE_STORAGE_KEY -else - echo -e "\n***\n*** Skipping Managed Identity tests (TEST_AZURE_MANAGED_IDENTITY not set)\n***" -fi - # Test localization to a specified location export TRITON_AZURE_MOUNT_DIRECTORY=`pwd`/azure_localization_test diff --git a/qa/L0_storage_swiftstack/infer_test.py b/qa/L0_storage_swiftstack/infer_test.py index a263991ea1..922caeb900 100755 --- a/qa/L0_storage_swiftstack/infer_test.py +++ b/qa/L0_storage_swiftstack/infer_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -87,6 +87,9 @@ def _infer_exact_helper( input_dtype, output0_dtype, output1_dtype, + (input_size, 1, 1), + (input_size, 1, 1), + (input_size, 1, 1), ): if input_dtype == np.int8: _infer_exact_helper( @@ -119,6 +122,9 @@ def _infer_exact_helper( input_dtype, output0_dtype, output1_dtype, + (input_size,), + (input_size,), + (input_size,), ): _infer_exact_helper( self, diff --git a/qa/L0_torch_aoti/test.sh b/qa/L0_torch_aoti/test.sh deleted file mode 100755 index f37751c55e..0000000000 --- a/qa/L0_torch_aoti/test.sh +++ /dev/null @@ -1,148 +0,0 @@ -#!/bin/bash -# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -source ../common/util.sh - -if [[ "${DEBUG}" == "true" ]]; then - set -x -else - set +x -fi - -COLOR_DARK="\033[90m" -COLOR_ERROR="\033[31m" -COLOR_INFO="\033[94m" -COLOR_RESET="\033[0m" -COLOR_STATUS="\033[36m" -COLOR_SUCCESS="\033[32m" -COLOR_WARNING="\033[33m" -RET=0 - -REPO_VERSION=${NVIDIA_TRITON_SERVER_VERSION} -if [[ "$#" -ge 1 ]]; then - REPO_VERSION=$1 -fi -if [[ -z "$REPO_VERSION" ]]; then - echo -e "${COLOR_ERROR}Repository version must be specified${COLOR_RESET}" 1>&2 - echo -e "${COLOR_ERROR}\n***\n*** Test Failed\n***${COLOR_RESET}" 1>&2 - exit 1 -fi -if [[ ! -z "$TEST_REPO_ARCH" ]]; then - REPO_VERSION=${REPO_VERSION}_${TEST_REPO_ARCH} -fi - -export CUDA_VISIBLE_DEVICES=0 - -MODELDIR=${MODELDIR:=`pwd`/models} -DATADIR=${DATADIR:="/data/inferenceserver/${REPO_VERSION}"} -TRITON_DIR=${TRITON_DIR:="/opt/tritonserver"} -SERVER=${TRITON_DIR}/bin/tritonserver -BACKEND_DIR=${TRITON_DIR}/backends - -# PyTorch on SBSA requires libgomp to be loaded first. See the following -# GitHub issue for more information: -# https://github.com/pytorch/pytorch/issues/2575 -arch=`uname -m` -echo -e "${COLOR_DARK}Detected architecture: ${arch}${COLOR_RESET}" -if [[ "${arch}" == "aarch64" ]]; then - SERVER_LD_PRELOAD=/usr/lib/$(uname -m)-linux-gnu/libgomp.so.1 - echo -e "${COLOR_DARK}SERVER_LD_PRELOAD=${SERVER_LD_PRELOAD}${COLOR_RESET}" -fi - -# If BACKENDS not specified, set to all -BACKENDS=${BACKENDS:="pytorch"} -export BACKENDS - -# Copy the models into the model repository -echo -e "${COLOR_DARK}Setting up model repository in ${MODELDIR}${COLOR_RESET}" -rm -rf ${MODELDIR} && mkdir -p ${MODELDIR} -models=( - "torch_aoti_complex_index" - "torch_aoti_complex_named" - "torch_aoti_int8_int8" - "torch_aoti_int16_int16" - "torch_aoti_int32_int32" - "torch_aoti_int64_int64" - "torch_aoti_float16_float16" - "torch_aoti_float32_float32" - "torchvision_aoti" -) -for model in "${models[@]}"; do - cp -r ${DATADIR}/qa_model_repository/${model} ${MODELDIR}/${model} - echo -e "${COLOR_DARK}ls ${MODELDIR}/${model}${COLOR_RESET}" - ls -lha ${MODELDIR}/${model} -done -echo -e "${COLOR_DARK}ls ${MODELDIR}${COLOR_RESET}" -ls -lha ${MODELDIR} - -SERVER_ARGS="--model-repository=${MODELDIR} --log-verbose=1" -SERVER_LOG="./torch_aoti_complex_named-server.log" -CLIENT_LOG="./torch_aoti_complex_named-client.log" - -echo -e "${COLOR_DARK}Running ${SERVER} with model repository ${MODELDIR}${COLOR_RESET}" -run_server -if [[ "${SERVER_PID}" -eq 0 ]]; then - echo -e "${COLOR_ERROR}\n***\n*** Failed to start ${SERVER}\n***${COLOR_RESET}" &1>2 - cat ${SERVER_LOG} &1>2 - echo -e "\n" &1>2 - exit 1 -fi - -# Install torch framework -echo -e "${COLOR_DARK}Installing PyTorch framework required by tests${COLOR_RESET}" -pip install torch - -# Run the Tests -TEST_NAME="torch_aoti_infer_test" -python3 ./${TEST_NAME}.py >> ${CLIENT_LOG} 2>&1 -EXIT_CODE=$? -if [[ ${EXIT_CODE} -ne 0 ]]; then - echo -e "${COLOR_ERROR}\n***\n*** Test '${TEST_NAME}' Failed with exit code ${EXIT_CODE}\n***${COLOR_RESET}" &1>2 - cat ${CLIENT_LOG} &1>2 - echo -e "\n" &1>2 - RET=1 -else - echo -e "${COLOR_INFO}\n***\n*** Test '${TEST_NAME}' Passed\n***${COLOR_RESET}" -fi - -# Cleanup -echo -e "${COLOR_DARK}Killing server (pid: ${SERVER_PID})${COLOR_RESET}" -kill -s SIGINT ${SERVER_PID} -wait ${SERVER_PID} || true -echo -e "${COLOR_DARK}Removing model repository${COLOR_RESET}" -for model in "${models[@]}"; do - rm -rf ${MODELDIR}/${model} -done - -# Report results and exit. -if [[ ${RET} -ne 0 ]]; then - echo -e "${COLOR_ERROR}\n***\n*** Test Suite FAILED\n***${COLOR_RESET}" &1>2 -else - echo -e "${COLOR_SUCCESS}\n***\n*** Test Suite PASSED\n***${COLOR_RESET}" -fi - -exit ${RET} diff --git a/qa/L0_torch_aoti/torch_aoti_infer_test.py b/qa/L0_torch_aoti/torch_aoti_infer_test.py deleted file mode 100755 index 2b93f31a48..0000000000 --- a/qa/L0_torch_aoti/torch_aoti_infer_test.py +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/python -# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import sys - -sys.path.append("../common") - -import unittest - -import test_util as tu -import torch -import tritonclient.http as http - - -class TorchAotiTest(tu.TestResultCollector): - def _get_complex_input_shape(self): - return (1, 16) - - def _get_complex_output_shape(self): - return (1, 16) - - def _get_complex_input_data(self, shape): - return [ - torch.randint(low=0, high=127, size=shape, dtype=torch.int8).numpy(), - torch.randint(low=0, high=127, size=shape, dtype=torch.int8).numpy(), - torch.randint(low=0, high=127, size=shape, dtype=torch.int8).numpy(), - torch.randint(low=0, high=127, size=shape, dtype=torch.int8).numpy(), - ] - - def _get_simple_input_data(self, shape, io_type): - if io_type in [torch.int8, torch.int16, torch.int32, torch.int64]: - return torch.randint(low=0, high=127, size=shape, dtype=io_type).numpy() - elif io_type in [torch.float16, torch.float32, torch.float64]: - return torch.randn(size=shape, dtype=io_type).numpy() - else: - raise ValueError(f"Unsupported data type: {io_type}") - - def _get_torchvision_input_data(self, shape): - return torch.randn(size=shape, dtype=torch.float32).numpy() - - def _dtype_to_triton_dtype(self, dtype): - if dtype == torch.int8: - return "INT8" - elif dtype == torch.int16: - return "INT16" - elif dtype == torch.int32: - return "INT32" - elif dtype == torch.int64: - return "INT64" - elif dtype == torch.float16: - return "FP16" - elif dtype == torch.float32: - return "FP32" - else: - raise ValueError(f"Unsupported data type: {dtype}") - - def _get_simple_model_name(self, io_type): - if io_type == torch.int8: - return "torch_aoti_int8_int8" - elif io_type == torch.int16: - return "torch_aoti_int16_int16" - elif io_type == torch.int32: - return "torch_aoti_int32_int32" - elif io_type == torch.int64: - return "torch_aoti_int64_int64" - elif io_type == torch.float16: - return "torch_aoti_float16_float16" - elif io_type == torch.float32: - return "torch_aoti_float32_float32" - else: - raise ValueError(f"Unsupported data type: {io_type}") - - def test_complex_index(self): - MODEL_NAME = "torch_aoti_complex_index" - INPUT_SHAPE = self._get_complex_input_shape() - OUTPUT_SHAPE = self._get_complex_output_shape() - - input_data = self._get_complex_input_data(INPUT_SHAPE) - - with http.InferenceServerClient("localhost:8000") as client: - inputs = [ - http.InferInput("INPUT__0", input_data[0].shape, "INT8"), - http.InferInput("INPUT__1", input_data[1].shape, "INT8"), - http.InferInput("INPUT__2", input_data[2].shape, "INT8"), - http.InferInput("INPUT__3", input_data[3].shape, "INT8"), - ] - - inputs[0].set_data_from_numpy(input_data[0], binary_data=True) - inputs[1].set_data_from_numpy(input_data[1], binary_data=True) - inputs[2].set_data_from_numpy(input_data[2], binary_data=True) - inputs[3].set_data_from_numpy(input_data[3], binary_data=True) - - output_names = [ - "OUTPUT__0", - "OUTPUT__1", - "OUTPUT__2", - "OUTPUT__3", - "OUTPUT__4", - "OUTPUT__5", - ] - - outputs = [] - for output_name in output_names: - outputs.append(http.InferRequestedOutput(output_name, binary_data=True)) - - output_data = [] - results = client.infer(MODEL_NAME, inputs, outputs=outputs) - - for output_name in output_names: - output_data.append(results.as_numpy(output_name)) - - self.assertEqual(len(outputs), len(output_data)) - for data in output_data: - self.assertEqual(data.shape, OUTPUT_SHAPE) - - self.assertTrue((output_data[0] == (input_data[0] + input_data[1])).all()) - self.assertTrue((output_data[1] == input_data[0] - input_data[1]).all()) - self.assertTrue((output_data[2] == input_data[0]).all()) - self.assertTrue((output_data[3] == input_data[1]).all()) - self.assertTrue((output_data[4] == input_data[2]).all()) - self.assertTrue((output_data[5] == input_data[3]).all()) - - def test_complex_named(self): - MODEL_NAME = "torch_aoti_complex_named" - INPUT_SHAPE = self._get_complex_input_shape() - OUTPUT_SHAPE = self._get_complex_output_shape() - - input_data = self._get_complex_input_data(INPUT_SHAPE) - - with http.InferenceServerClient("localhost:8000") as client: - inputs = [ - http.InferInput("ARGS[0]", input_data[0].shape, "INT8"), - http.InferInput("ARGS[1]", input_data[1].shape, "INT8"), - http.InferInput("ARGS[2][option1]", input_data[2].shape, "INT8"), - http.InferInput("ARGS[2][option2]", input_data[3].shape, "INT8"), - ] - - inputs[0].set_data_from_numpy(input_data[0], binary_data=True) - inputs[1].set_data_from_numpy(input_data[1], binary_data=True) - inputs[2].set_data_from_numpy(input_data[2], binary_data=True) - inputs[3].set_data_from_numpy(input_data[3], binary_data=True) - - output_names = [ - "RESULT[AAA]", - "RESULT[BBB][0]", - "RESULT[BBB][1]", - "RESULT[CCC][option1]", - "RESULT[CCC][option2]", - "RESULT[ZZZ]", - ] - - outputs = [] - for output_name in output_names: - outputs.append(http.InferRequestedOutput(output_name, binary_data=True)) - - output_data = [] - results = client.infer(MODEL_NAME, inputs, outputs=outputs) - - for output_name in output_names: - output_data.append(results.as_numpy(output_name)) - - self.assertEqual(len(outputs), len(output_data)) - for data in output_data: - self.assertEqual(data.shape, OUTPUT_SHAPE) - - self.assertTrue((output_data[0] == (input_data[0] + input_data[1])).all()) - self.assertTrue((output_data[1] == input_data[0]).all()) - self.assertTrue((output_data[2] == input_data[1]).all()) - self.assertTrue((output_data[3] == input_data[2]).all()) - self.assertTrue((output_data[4] == input_data[3]).all()) - self.assertTrue((output_data[5] == (input_data[0] - input_data[1])).all()) - - def test_simple_model(self): - io_types = [ - torch.int8, - torch.int16, - torch.int32, - torch.int64, - torch.float16, - torch.float32, - ] - for io_type in io_types: - MODEL_NAME = self._get_simple_model_name(io_type) - INPUT_SHAPE = (16,) - OUTPUT_SHAPE = (16,) - TRITON_IO_TYPE = self._dtype_to_triton_dtype(io_type) - - input_data = ( - self._get_simple_input_data(INPUT_SHAPE, io_type), - self._get_simple_input_data(INPUT_SHAPE, io_type), - ) - - with http.InferenceServerClient("localhost:8000") as client: - inputs = [ - http.InferInput("ARGS[0]", input_data[0].shape, TRITON_IO_TYPE), - http.InferInput("ARGS[1]", input_data[1].shape, TRITON_IO_TYPE), - ] - - inputs[0].set_data_from_numpy(input_data[0], binary_data=True) - inputs[1].set_data_from_numpy(input_data[1], binary_data=True) - - output_names = [ - "RESULT", - ] - - outputs = [] - for output_name in output_names: - outputs.append( - http.InferRequestedOutput(output_name, binary_data=True) - ) - - output_data = [] - results = client.infer(MODEL_NAME, inputs, outputs=outputs) - - for output_name in output_names: - output_data.append(results.as_numpy(output_name)) - - self.assertEqual(len(outputs), len(output_data)) - for data in output_data: - self.assertEqual(data.shape, OUTPUT_SHAPE) - self.assertTrue((data == input_data[0] + input_data[1]).all()) - - def test_torchvision(self): - MODEL_NAME = "torchvision_aoti" - INPUT_SHAPE = (1, 3, 224, 224) - OUTPUT_SHAPE = (1, 1000) - - input_data = self._get_torchvision_input_data(INPUT_SHAPE) - input_data[0][0] = 1.0 - - with http.InferenceServerClient("localhost:8000") as client: - inputs = [ - http.InferInput("ARGS[0]", input_data.shape, "FP32"), - ] - - inputs[0].set_data_from_numpy(input_data, binary_data=True) - - output_names = [ - "RESULT", - ] - - outputs = [] - for output_name in output_names: - outputs.append(http.InferRequestedOutput(output_name, binary_data=True)) - - output_data = [] - results = client.infer(MODEL_NAME, inputs, outputs=outputs) - - for output_name in output_names: - output_data.append(results.as_numpy(output_name)) - - self.assertEqual(len(outputs), len(output_data)) - for data in output_data: - self.assertEqual(data.shape, OUTPUT_SHAPE) - output_tensor = torch.from_numpy(data) - self.assertTrue(torch.isfinite(output_tensor).all().item()) - - -if __name__ == "__main__": - unittest.main() diff --git a/qa/L0_trace/test.sh b/qa/L0_trace/test.sh index 7b29ea017a..ce53ea8528 100755 --- a/qa/L0_trace/test.sh +++ b/qa/L0_trace/test.sh @@ -109,11 +109,6 @@ mkdir -p $MODELSDIR/custom_identity_int32/1 && (cd $MODELSDIR/custom_identity_in RET=0 -# set up identity_fp32 model -mkdir -p $MODELSDIR/identity_fp32/1 && \ - cp ../python_models/identity_fp32/model.py $MODELSDIR/identity_fp32/1/. && \ - cp ../python_models/identity_fp32/config.pbtxt $MODELSDIR/identity_fp32/. - # Helpers ======================================= function assert_curl_success { message="${1}" @@ -191,26 +186,6 @@ function send_inference_requests { done } -function run_stress_client { - stress_client="${1}" - client_log="${2}" - echo "Running stress test for 120 seconds..." - bash -c ' - # Handle SIGTERM (signal 15) and exit gracefully - trap "echo \"cleaning up stress client...\"; exit 0" SIGTERM - - while true; do - python3 "$1" >> "$2" - sleep 0.1 - done' _ "$stress_client" "$client_log" & CLIENT_PID=$! - sleep 120 - - set -e - kill $CLIENT_PID - wait $CLIENT_PID - set +e -} - #======================================= # start with trace-level=OFF @@ -1087,9 +1062,6 @@ for p in {1..10}; do sleep 10 done -# Wait for all traces to be collected -sleep 5 - if ! [[ -s collected_traces.json && `grep -c "\"parentSpanId\":\"\"" ./collected_traces.json` == 1 && `grep -c "\"parentSpanId\":\"b7ad6b7169242424\"" ./collected_traces.json` == 10 ]] ; then echo -e "\n***\n*** collected_traces.json should contain 11 OTel trace, but it is not. \n***" exit 1 @@ -1272,78 +1244,4 @@ set -e kill $SERVER_PID wait $SERVER_PID set +e - -# Long running stress test -# Triton trace mode -SERVER_ARGS="--model-control-mode=explicit \ - --model-repository=$MODELSDIR \ - --load-model=identity_fp32 \ - --trace-config mode=triton \ - --trace-config triton,file=./trace \ - --trace-config rate=1 \ - --trace-config level=TIMESTAMPS" -SERVER_LOG="./inference_server_triton_trace_stress.log" -CLIENT_LOG="./client_triton_trace_stress.log" -STRESS_CLIENT="./trace_stress_grpc_client.py" - -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 -fi - -# Run stress test -run_stress_client $STRESS_CLIENT $CLIENT_LOG - -set -e -if ! kill -0 ${SERVER_PID} > /dev/null 2>&1; then - echo -e "\n***\n*** Server stopped unexpectedly during stress test\n***" - cat $SERVER_LOG - RET=1 -else - kill $SERVER_PID - wait $SERVER_PID -fi -set +e - -# Opentelemetry trace mode -SERVER_ARGS="--model-control-mode=explicit \ - --model-repository=$MODELSDIR \ - --load-model=identity_fp32 \ - --trace-config level=TIMESTAMPS \ - --trace-config rate=1 \ - --trace-config mode=opentelemetry \ - --trace-config opentelemetry,resource=test.key=test.value \ - --trace-config opentelemetry,resource=service.name=test_triton \ - --trace-config opentelemetry,url=localhost:$OTLP_PORT/v1/traces" -SERVER_LOG="./inference_server_otel_trace_stress.log" -CLIENT_LOG="./client_otel_trace_stress.log" -STRESS_CLIENT="./trace_stress_grpc_client.py" - -run_server -if [ "$SERVER_PID" == "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 -fi - -rm collected_traces.json -$OTEL_COLLECTOR --config ./trace-config.yaml >> $OTEL_COLLECTOR_LOG 2>&1 & COLLECTOR_PID=$! -# Run stress test -run_stress_client $STRESS_CLIENT $CLIENT_LOG - -set -e -kill $COLLECTOR_PID -wait $COLLECTOR_PID -if ! kill -0 ${SERVER_PID} > /dev/null 2>&1; then - echo -e "\n***\n*** Server stopped unexpectedly during stress test\n***" - cat $SERVER_LOG - RET=1 -else - kill $SERVER_PID - wait $SERVER_PID -fi -set +e - exit $RET diff --git a/qa/L0_trace/trace_stress_grpc_client.py b/qa/L0_trace/trace_stress_grpc_client.py deleted file mode 100755 index 99a676f150..0000000000 --- a/qa/L0_trace/trace_stress_grpc_client.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import random -import sys -import time -from functools import partial - -import numpy as np -import tritonclient.grpc as grpcclient - -if __name__ == "__main__": - # 1 ms cancellation timeout - client_timeout = 1 - url = "localhost:8001" - - try: - triton_client = grpcclient.InferenceServerClient(url=url) - except Exception as e: - print("context creation failed: " + str(e)) - sys.exit() - - model_name = "identity_fp32" - - # Infer - inputs = [] - - input_data = np.array( - [random.random() for i in range(50)], dtype=np.float32 - ).reshape(1, -1) - model_input = grpcclient.InferInput( - name="INPUT0", datatype="FP32", shape=input_data.shape - ) - model_input.set_data_from_numpy(input_data) - inputs.append(model_input) - - # Define the callback function. Note the last two parameters should be - # result and error. InferenceServerClient would povide the results of an - # inference as grpcclient.InferResult in result. For successful - # inference, error will be None, otherwise it will be an object of - # tritonclientutils.InferenceServerException holding the error details - def callback(user_data, result, error): - if error: - user_data.append(error) - else: - user_data.append(result) - - # list to hold the results of inference. - user_data = [] - - # Inference call - for _ in range(1000): - triton_client.async_infer( - model_name=model_name, - inputs=inputs, - callback=partial(callback, user_data), - client_timeout=client_timeout, - ) - - # Wait until the results are available in user_data - time_out = 20 - while (len(user_data) == 0) and time_out > 0: - time_out = time_out - 1 - time.sleep(1) - - print("results: ", len(user_data)) diff --git a/qa/L0_trt_bf16_dtype/trt_bf16_dtype_test.py b/qa/L0_trt_bf16_dtype/trt_bf16_dtype_test.py index c80c84d353..265c1930b0 100755 --- a/qa/L0_trt_bf16_dtype/trt_bf16_dtype_test.py +++ b/qa/L0_trt_bf16_dtype/trt_bf16_dtype_test.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -32,7 +32,6 @@ import unittest -import ml_dtypes import numpy as np import test_util as tu import tritonclient.http as client @@ -50,9 +49,8 @@ def _infer_helper(self, model_name, shape): inputs.append(client.InferInput("INPUT0", shape, "BF16")) inputs.append(client.InferInput("INPUT1", shape, "BF16")) - # BF16 inputs use ml_dtypes.bfloat16; numpy has no native BF16 dtype. - input0_data = np.ones(shape, dtype=ml_dtypes.bfloat16) - input1_data = np.ones(shape, dtype=ml_dtypes.bfloat16) + input0_data = np.ones(shape=shape).astype(np.float32) + input1_data = np.ones(shape=shape).astype(np.float32) inputs[0].set_data_from_numpy(input0_data, binary_data=True) inputs[1].set_data_from_numpy(input1_data, binary_data=True) @@ -65,8 +63,6 @@ def _infer_helper(self, model_name, shape): output0_data = results.as_numpy("OUTPUT0") output1_data = results.as_numpy("OUTPUT1") - self.assertEqual(output0_data.dtype, ml_dtypes.bfloat16) - self.assertEqual(output1_data.dtype, ml_dtypes.bfloat16) np.testing.assert_equal( output0_data, input0_data + input1_data, diff --git a/qa/L0_trt_dynamic_shape/test.sh b/qa/L0_trt_dynamic_shape/test.sh index 42928969fd..43a39dd199 100755 --- a/qa/L0_trt_dynamic_shape/test.sh +++ b/qa/L0_trt_dynamic_shape/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -41,10 +41,8 @@ fi TEST_RESULT_FILE='test_results.txt' export CUDA_VISIBLE_DEVICES=0 -pip3 install perf_analyzer - CLIENT_LOG="./client.log" -PERF_CLIENT=perf_analyzer +PERF_CLIENT=../clients/perf_client TRT_OP_TEST=trt_dynamic_shape_test.py DATADIR="./models" @@ -72,9 +70,11 @@ fi # Shape beyond the limits of optimization profile set +e $PERF_CLIENT -v -i grpc -u localhost:8001 -m plan_float32_float32_float32-4-32 --shape INPUT0:33 --shape INPUT1:33 -t 1 -p2000 -b 1 > ${CLIENT_LOG}_max 2>&1 -EXIT_CODE=$? -echo "perf_analyzer exit code: ${EXIT_CODE}" >> "${CLIENT_LOG}_max" -"${PERF_CLIENT}" --version >> "${CLIENT_LOG}_max" 2>&1 || true +if [ $? -eq 0 ]; then + cat ${CLIENT_LOG}_max + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi EXPECTED_MESSAGE="model expected the shape of dimension 1 to be between 4 and 32 but received" if [ $(cat ${CLIENT_LOG}_max | grep "${EXPECTED_MESSAGE} 33" | wc -l) -eq 0 ]; then @@ -84,10 +84,11 @@ if [ $(cat ${CLIENT_LOG}_max | grep "${EXPECTED_MESSAGE} 33" | wc -l) -eq 0 ]; t fi $PERF_CLIENT -v -i grpc -u localhost:8001 -m plan_float32_float32_float32-4-32 --shape INPUT0:3 --shape INPUT1:3 -t 1 -p2000 -b 1 > ${CLIENT_LOG}_min 2>&1 -EXIT_CODE=$? -echo "perf_analyzer exit code: ${EXIT_CODE}" >> "${CLIENT_LOG}_min" -"${PERF_CLIENT}" --version >> "${CLIENT_LOG}_min" 2>&1 || true - +if [ $? -eq 0 ]; then + cat ${CLIENT_LOG}_min + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi if [ $(cat ${CLIENT_LOG}_min | grep "${EXPECTED_MESSAGE} 3" | wc -l) -eq 0 ]; then cat ${CLIENT_LOG}_min echo -e "\n***\n*** Test Failed\n***" @@ -330,10 +331,11 @@ if [ $? -ne 0 ]; then fi $PERF_CLIENT -v -i grpc -u localhost:8001 -m plan_float32_float32_float32 --shape INPUT0:33 --shape INPUT1:33 -t 1 -p2000 -b 6 > ${CLIENT_LOG}_static_fail 2>&1 -EXIT_CODE=$? -echo "perf_analyzer exit code: ${EXIT_CODE}" >> "${CLIENT_LOG}_static_fail" -"${PERF_CLIENT}" --version >> "${CLIENT_LOG}_static_fail" 2>&1 || true - +if [ $? -eq 0 ]; then + ${CLIENT_LOG}_static_fail + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi if [ $(cat ${CLIENT_LOG}_static_fail | grep "inference request batch-size must be <= 5" | wc -l) -eq 0 ]; then cat ${CLIENT_LOG}_static_fail echo -e "\n***\n*** Test Failed\n***" @@ -341,10 +343,11 @@ if [ $(cat ${CLIENT_LOG}_static_fail | grep "inference request batch-size must b fi $PERF_CLIENT -v -i grpc -u localhost:8001 -m plan_float32_float32_float32 --shape INPUT0:33 --shape INPUT1:33 -t 1 -p2000 -b 2 > ${CLIENT_LOG}_static_bs_2 2>&1 -EXIT_CODE=$? -echo "perf_analyzer exit code: ${EXIT_CODE}" >> "${CLIENT_LOG}_static_bs_2" -"${PERF_CLIENT}" --version >> "${CLIENT_LOG}_static_bs_2" 2>&1 || true - +if [ $? -eq 0 ]; then + ${CLIENT_LOG}_static_bs_2 + echo -e "\n***\n*** Test Failed\n***" + RET=1 +fi if [ $(cat ${CLIENT_LOG}_static_bs_2 | grep "model expected the shape of dimension 0 to be between 1 and 1 but received 2" | wc -l) -eq 0 ]; then cat ${CLIENT_LOG}_static_bs_2 echo -e "\n***\n*** Test Failed\n***" diff --git a/qa/L0_trt_reformat_free/test.sh b/qa/L0_trt_reformat_free/test.sh index 4cf3198ec2..2daf2f0648 100755 --- a/qa/L0_trt_reformat_free/test.sh +++ b/qa/L0_trt_reformat_free/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2019-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -50,7 +50,7 @@ rm -rf ${DATADIR} cp -r /data/inferenceserver/${REPO_VERSION}/qa_trt_format_model_repository/ ${DATADIR} SERVER=/opt/tritonserver/bin/tritonserver -SERVER_ARGS="--model-repository=$DATADIR --allow-client-shm=true" +SERVER_ARGS="--model-repository=$DATADIR" source ../common/util.sh rm -f *.log* diff --git a/qa/L0_trt_shape_tensors/test.sh b/qa/L0_trt_shape_tensors/test.sh index 9d03791007..548ebb55af 100755 --- a/qa/L0_trt_shape_tensors/test.sh +++ b/qa/L0_trt_shape_tensors/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2019-2026, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2019-2024, NVIDIA CORPORATION. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -45,18 +45,10 @@ CLIENT_LOG="./client.log" SHAPE_TENSOR_TEST=trt_shape_tensor_test.py SERVER=/opt/tritonserver/bin/tritonserver -SERVER_ARGS="--model-repository=`pwd`/models --log-verbose=1 --allow-client-shm=true" +SERVER_ARGS="--model-repository=`pwd`/models --log-verbose=1" SERVER_LOG="./inference_server.log" source ../common/util.sh -if [ -z "$TEST_SYSTEM_SHARED_MEMORY" ]; then - TEST_SYSTEM_SHARED_MEMORY="0" -fi - -if [ -z "$TEST_CUDA_SHARED_MEMORY" ]; then - TEST_CUDA_SHARED_MEMORY="0" -fi - rm -fr *.log rm -fr models && mkdir models cp -r /data/inferenceserver/${REPO_VERSION}/qa_shapetensor_model_repository/* models/. @@ -228,9 +220,6 @@ for i in \ test_dynaseq_different_shape_values_parallel \ ;do SERVER_ARGS="--model-repository=`pwd`/models" - if [ "$TEST_SYSTEM_SHARED_MEMORY" -eq 1 ] || [ "$TEST_CUDA_SHARED_MEMORY" -eq 1 ]; then - SERVER_ARGS="${SERVER_ARGS} --allow-client-shm=true" - fi SERVER_LOG="./$i.server.log" run_server if [ "$SERVER_PID" == "0" ]; then diff --git a/qa/L0_vertex_ai/test.sh b/qa/L0_vertex_ai/test.sh index c8da5ed57d..7403bf14cf 100755 --- a/qa/L0_vertex_ai/test.sh +++ b/qa/L0_vertex_ai/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -44,7 +44,7 @@ export CUDA_VISIBLE_DEVICES=0 RET=0 -rm -rf multi_models single_model restricted_single_model +rm -rf multi_models single_model rm -f *.log rm -f *.out @@ -68,11 +68,6 @@ mkdir multi_models && \ mkdir single_model && \ cp -r multi_models/addsub single_model/. -# Set up single-model Python repository used by restricted API regression -mkdir -p restricted_single_model/identity_fp32/1 && \ - cp ../python_models/identity_fp32/config.pbtxt restricted_single_model/identity_fp32/ && \ - cp ../python_models/identity_fp32/model.py restricted_single_model/identity_fp32/1/ - # Use Vertex AI's health endpoint to check server status # Wait until server health endpoint shows ready. Sets WAIT_RET to 0 on # success, 1 on failure @@ -139,7 +134,7 @@ if [ "$SERVER_PID" == "0" ]; then exit 1 fi kill $SERVER_PID -wait $SERVER_PID +wait $SERVE_PID set +e # Expect no message regarding Vertex AI as it is disabled grep "failed to start Vertex AI service" $SERVER_LOG @@ -162,7 +157,7 @@ if [ "$SERVER_PID" == "0" ]; then exit 1 fi kill $SERVER_PID -wait $SERVER_PID +wait $SERVE_PID set +e grep "Started Vertex AI HTTPService at" $SERVER_LOG if [ $? -ne 0 ]; then @@ -191,7 +186,7 @@ if [ "$SERVER_PID" == "0" ]; then exit 1 fi kill $SERVER_PID -wait $SERVER_PID +wait $SERVE_PID set +e grep "Started Vertex AI HTTPService at" $SERVER_LOG if [ $? -ne 0 ]; then @@ -225,7 +220,7 @@ if [ "$SERVER_PID" == "0" ]; then exit 1 fi kill $SERVER_PID -wait $SERVER_PID +wait $SERVE_PID set +e # Expect no message regarding Vertex AI as it is disabled grep "failed to start Vertex AI service" $SERVER_LOG @@ -336,7 +331,7 @@ fi set -e kill $SERVER_PID -wait $SERVER_PID +wait $SERVE_PID # # AIP_STORAGE_URI / AIP_HTTP_PORT @@ -375,7 +370,7 @@ fi set -e kill $SERVER_PID -wait $SERVER_PID +wait $SERVE_PID # # default model @@ -458,7 +453,7 @@ else fi set -e kill $SERVER_PID -wait $SERVER_PID +wait $SERVE_PID # Test default model as well as multi model SERVER_ARGS="--vertex-ai-default-model=addsub" @@ -699,247 +694,25 @@ else fi set -e -# repository control via redirect is blocked unconditionally +# repository control (expect error) rm -f ./curl.out set +e code=`curl -s -w %{http_code} -o ./curl.out -X POST -H "X-Vertex-Ai-Triton-Redirect: v2/repository/models/subadd/unload" localhost:8080/predict` -if [ "$code" != "403" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected model unload via redirect to return 403 (got $code)\n***" - RET=1 -fi -set -e - -kill $SERVER_PID -wait $SERVER_PID - -# -# Restricted API regression for Vertex redirect -# -unset_vertex_variables -export AIP_PREDICT_ROUTE="/predict" -export AIP_HEALTH_ROUTE="/health" - -SERVER_LOG="vertex_restricted_api_testing_server.log" -SERVER_ARGS="--log-verbose=1 --allow-vertex-ai=true \ - --model-repository=restricted_single_model \ - --vertex-ai-default-model=identity_fp32 \ - --http-restricted-api=metadata,model-config,model-repository,statistics,shared-memory:X-Vertex-Restricted=secret" - -run_server_nowait -vertex_ai_wait_for_server_ready $SERVER_PID 10 -if [ "$WAIT_RET" != "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - kill $SERVER_PID - wait $SERVER_PID - cat $SERVER_LOG - exit 1 -fi - -# Baseline infer remains available without restricted header -set +e -rm -f ./curl.out -code=`curl -s -w %{http_code} -o ./curl.out -X POST -H "Content-Type: application/json" -d'{"inputs":[{"name":"INPUT0","datatype":"FP32","shape":[1,1],"data":[42.0]}],"outputs":[{"name":"OUTPUT0"}]}' localhost:8080/predict` -if [ "$code" != "200" ]; then +if [ "$code" == "200" ]; then cat ./curl.out - echo -e "\n***\n*** Failed. Expected /predict inference succeeds without restricted header\n***" + echo -e "\n***\n*** Test Failed\n***" RET=1 else - grep "OUTPUT0" ./curl.out + grep "explicit model load / unload is not allowed" ./curl.out if [ $? -ne 0 ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected inference output is returned\n***" + echo -e "\n***\n*** Failed. Expected error on model control\n***" RET=1 fi fi set -e -# Redirected read-only APIs should be blocked without restricted header -set +e -for redirect_endpoint in \ - "v2" \ - "v2/models/identity_fp32/config" \ - "v2/repository/index" \ - "v2/systemsharedmemory/status" \ - "v2/cudasharedmemory/status" -do - rm -f ./curl.out - code=`curl -s -w %{http_code} -o ./curl.out -X POST -H "X-Vertex-Ai-Triton-Redirect: ${redirect_endpoint}" localhost:8080/predict` - if [ "$code" != "403" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected ${redirect_endpoint} is restricted\n***" - RET=1 - fi -done - -# Mutating shared memory operations are blocked through redirect -rm -f ./curl.out -code=`curl -s -w %{http_code} -o ./curl.out -X POST -H "X-Vertex-Ai-Triton-Redirect: v2/systemsharedmemory/region/test/register" -H "Content-Type: application/json" -d '{"key":"test_shm","byte_size":1024}' localhost:8080/predict` -if [ "$code" != "403" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected shared memory register is blocked via redirect\n***" - RET=1 -fi - -# Model load redirect is unconditionally blocked through the prediction endpoint -rm -f ./curl.out -code=`curl -s -w %{http_code} -o ./curl.out -X POST -H "X-Vertex-Ai-Triton-Redirect: v2/repository/models/identity_fp32/load" localhost:8080/predict` -if [ "$code" != "403" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected model load via redirect is blocked\n***" - RET=1 -fi - -# Model unload redirect is unconditionally blocked through the prediction endpoint -rm -f ./curl.out -code=`curl -s -w %{http_code} -o ./curl.out -X POST -H "X-Vertex-Ai-Triton-Redirect: v2/repository/models/identity_fp32/unload" localhost:8080/predict` -if [ "$code" != "403" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected model unload via redirect is blocked\n***" - RET=1 -fi - -# Statistics redirect without restricted header should be blocked -rm -f ./curl.out -code=`curl -s -w %{http_code} -o ./curl.out -X POST -H "X-Vertex-Ai-Triton-Redirect: v2/models/identity_fp32/stats" localhost:8080/predict` -if [ "$code" != "403" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected model statistics via redirect is restricted\n***" - RET=1 -fi -set -e - -# Restricted header should allow read-only handler invocation -set +e -for redirect_endpoint in \ - "v2" \ - "v2/models/identity_fp32/config" \ - "v2/repository/index" \ - "v2/systemsharedmemory/status" \ - "v2/cudasharedmemory/status" -do - rm -f ./curl.out - code=`curl -s -w %{http_code} -o ./curl.out -X POST -H "X-Vertex-Ai-Triton-Redirect: ${redirect_endpoint}" \ - -H "X-Vertex-Restricted: secret" localhost:8080/predict` - if [ "$code" != "200" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected ${redirect_endpoint} passes with restricted header\n***" - RET=1 - fi -done - -# Mutating shared memory operations remain blocked even with valid header -rm -f ./curl.out -code=`curl -s -w %{http_code} -o ./curl.out -X POST -H "X-Vertex-Ai-Triton-Redirect: v2/systemsharedmemory/region/test/register" -H "X-Vertex-Restricted: secret" -H "Content-Type: application/json" -d '{"key":"test_shm","byte_size":1024}' localhost:8080/predict` -if [ "$code" != "403" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected shared memory register is blocked via redirect even with valid header\n***" - RET=1 -fi - -# Model load remains blocked even with valid restricted header -rm -f ./curl.out -code=`curl -s -w %{http_code} -o ./curl.out -X POST -H "X-Vertex-Ai-Triton-Redirect: v2/repository/models/identity_fp32/load" -H "X-Vertex-Restricted: secret" localhost:8080/predict` -if [ "$code" != "403" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected model load is blocked via redirect even with valid header\n***" - RET=1 -fi - -# Model unload remains blocked even with valid restricted header -rm -f ./curl.out -code=`curl -s -w %{http_code} -o ./curl.out -X POST -H "X-Vertex-Ai-Triton-Redirect: v2/repository/models/identity_fp32/unload" -H "X-Vertex-Restricted: secret" localhost:8080/predict` -if [ "$code" != "403" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected model unload is blocked via redirect even with valid header\n***" - RET=1 -fi - -# Statistics redirect with restricted header should pass -rm -f ./curl.out -code=`curl -s -w %{http_code} -o ./curl.out -X POST -H "X-Vertex-Ai-Triton-Redirect: v2/models/identity_fp32/stats" -H "X-Vertex-Restricted: secret" localhost:8080/predict` -if [ "$code" == "403" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected model statistics passes with restricted header\n***" - RET=1 -fi - -# Wrong restricted header should reject the request -set +e -for redirect_endpoint in \ - "v2" \ - "v2/models/identity_fp32/config" \ - "v2/repository/index" \ - "v2/systemsharedmemory/status" \ - "v2/cudasharedmemory/status" \ - "v2/systemsharedmemory/region/test/register" -do - rm -f ./curl.out - if [ "$redirect_endpoint" != "v2/systemsharedmemory/region/test/register" ]; then - code=`curl -s -w %{http_code} -o ./curl.out -X POST -H "X-Vertex-Ai-Triton-Redirect: ${redirect_endpoint}" \ - -H "X-Vertex-Restricted: invalid" localhost:8080/predict` - else - code=`curl -s -w %{http_code} -o ./curl.out -X POST -H "X-Vertex-Ai-Triton-Redirect: ${redirect_endpoint}" \ - -H "X-Vertex-Restricted: invalid" -H "Content-Type: application/json" -d '{"key":"test_shm","byte_size":1024}' localhost:8080/predict` - fi - if [ "$code" != "403" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected invalid restricted header value is rejected for ${redirect_endpoint}\n***" - RET=1 - fi -done -set -e - -kill $SERVER_PID -wait $SERVER_PID - -# -# HTTP max input size enforcement on Vertex AI endpoint -# -unset_vertex_variables -export AIP_PREDICT_ROUTE="/predict" -export AIP_HEALTH_ROUTE="/health" - -SERVER_LOG="vertex_max_input_size_server.log" -SERVER_ARGS="--allow-vertex-ai=true \ - --model-repository=restricted_single_model \ - --vertex-ai-default-model=identity_fp32 \ - --http-max-input-size=128" -run_server_nowait -vertex_ai_wait_for_server_ready $SERVER_PID 10 -if [ "$WAIT_RET" != "0" ]; then - echo -e "\n***\n*** Failed to start $SERVER\n***" - kill $SERVER_PID - wait $SERVER_PID - cat $SERVER_LOG - exit 1 -fi - -set +e - -# Small payload under 128 bytes should succeed -rm -f ./curl.out -code=`curl -s -w %{http_code} -o ./curl.out -X POST -H "Content-Type: application/json" -d'{"inputs":[{"name":"INPUT0","datatype":"FP32","shape":[1,1],"data":[1.0]}],"outputs":[{"name":"OUTPUT0"}]}' localhost:8080/predict` -if [ "$code" != "200" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected small payload to succeed on Vertex AI endpoint (got $code)\n***" - RET=1 -fi - -# Large payload over 128 bytes should be rejected -rm -f ./curl.out -LARGE_PAYLOAD='{"inputs":[{"name":"INPUT0","datatype":"FP32","shape":[1,16],"data":[1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0,14.0,15.0,16.0]}],"outputs":[{"name":"OUTPUT0"}]}' -code=`curl -s -w %{http_code} -o ./curl.out -X POST -H "Content-Type: application/json" -d"$LARGE_PAYLOAD" localhost:8080/predict` -if [ "$code" == "200" ]; then - cat ./curl.out - echo -e "\n***\n*** Failed. Expected oversized payload to be rejected on Vertex AI endpoint\n***" - RET=1 -fi - -set -e - kill $SERVER_PID -wait $SERVER_PID +wait $SERVE_PID if [ $RET -eq 0 ]; then echo -e "\n***\n*** Test Passed\n***" diff --git a/qa/L0_warmup/test.sh b/qa/L0_warmup/test.sh index 84a0752229..c3e6885062 100755 --- a/qa/L0_warmup/test.sh +++ b/qa/L0_warmup/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -420,7 +420,7 @@ wait $SERVER_PID # Test the onnx model to verify that the memory type of the output tensor # remains unchanged with the warmup setting pip3 uninstall -y torch -pip3 install torch -f https://download.pytorch.org/whl/cu130 +pip3 install torch==2.3.1+cu118 -f https://download.pytorch.org/whl/torch_stable.html rm -fr models && mkdir models cp -r /data/inferenceserver/${REPO_VERSION}/qa_model_repository/onnx_nobatch_float32_float32_float32 models/. diff --git a/qa/common/check_copyright.py b/qa/common/check_copyright.py index de3bdc79b4..99606a1c81 100755 --- a/qa/common/check_copyright.py +++ b/qa/common/check_copyright.py @@ -75,7 +75,6 @@ "qa/L0_model_config/cli_messages/cli_override/expected", "qa/L0_model_config/cli_messages/cli_deprecation/expected", "qa/L0_model_config/model_metrics", - "qa/L0_model_config/custom_parameters", "qa/L0_model_namespacing/test_duplication", "qa/L0_model_namespacing/test_dynamic_resolution", "qa/L0_model_namespacing/test_ensemble_duplication", diff --git a/qa/common/check_valgrind_log.py b/qa/common/check_valgrind_log.py index 8a59dd3e9c..84eecd4989 100755 --- a/qa/common/check_valgrind_log.py +++ b/qa/common/check_valgrind_log.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -58,7 +58,7 @@ def check_valgrind_log(log_file): a list of the leak records as strings """ - with open(log_file, "r") as f: + with open(args.input_log_file, "r") as f: logs = f.read() # Find the pid and start and end of definite leak reports diff --git a/qa/common/gen_common.py b/qa/common/gen_common.py index db0869ef38..d53702d604 100644 --- a/qa/common/gen_common.py +++ b/qa/common/gen_common.py @@ -1,4 +1,4 @@ -# Copyright 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -28,14 +28,13 @@ from typing import List # Common utilities for model generation scripts -import ml_dtypes import numpy as np np_dtype_string = np.dtype(object) # Numpy does not support the BF16 datatype natively. -# We use ml_dtypes.bfloat16 for BF16. -np_dtype_bfloat16 = ml_dtypes.bfloat16 +# We use this dummy dtype as a representative for BF16. +np_dtype_bfloat16 = np.dtype([("bf16", object)]) def np_to_onnx_dtype(np_dtype): @@ -63,8 +62,6 @@ def np_to_onnx_dtype(np_dtype): return onnx.TensorProto.DOUBLE elif np_dtype == np_dtype_string: return onnx.TensorProto.STRING - elif np_dtype == np_dtype_bfloat16: - return onnx.TensorProto.BFLOAT16 return None diff --git a/qa/common/gen_ensemble_model_utils.py b/qa/common/gen_ensemble_model_utils.py index 1dae50a380..fec8f0bf92 100755 --- a/qa/common/gen_ensemble_model_utils.py +++ b/qa/common/gen_ensemble_model_utils.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -761,7 +761,7 @@ def create_ensemble_modelfile( try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir @@ -834,7 +834,7 @@ def create_ensemble_modelconfig( try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(config_dir + "/config.pbtxt", "w") as cfile: @@ -867,7 +867,7 @@ def create_identity_ensemble_modelfile( try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir @@ -915,7 +915,7 @@ def create_identity_ensemble_modelconfig( try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(config_dir + "/config.pbtxt", "w") as cfile: @@ -937,7 +937,7 @@ def create_sequence_ensemble_modelfile( try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir @@ -979,7 +979,7 @@ def create_sequence_ensemble_modelconfig( try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(config_dir + "/config.pbtxt", "w") as cfile: @@ -1015,7 +1015,7 @@ def create_nop_modelconfig( try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(config_dir + "/config.pbtxt", "w") as cfile: @@ -1059,7 +1059,7 @@ def create_nop_tunnel_modelconfig(models_dir, tensor_shape, tensor_dtype): try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(config_dir + "/config.pbtxt", "w") as cfile: @@ -1084,7 +1084,7 @@ def create_nop_tunnel_modelconfig(models_dir, tensor_shape, tensor_dtype): try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(config_dir + "/config.pbtxt", "w") as cfile: diff --git a/qa/common/gen_jetson_trt_models b/qa/common/gen_jetson_trt_models index 4d491fa2a1..1dda5b72cf 100755 --- a/qa/common/gen_jetson_trt_models +++ b/qa/common/gen_jetson_trt_models @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2020-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -34,15 +34,15 @@ # Make all generated files accessible outside of container umask 0000 # Set the version of the models -TRITON_VERSION=${TRITON_VERSION:=26.05} +TRITON_VERSION=${TRITON_VERSION:=25.03} # Set the CUDA device to use -NVIDIA_VISIBLE_DEVICES=${NVIDIA_VISIBLE_DEVICES:=0} +CUDA_DEVICE=${RUNNER_ID:=0} # Set TensorRT image TENSORRT_IMAGE=${TENSORRT_IMAGE:=nvcr.io/nvidia/tensorrt:$TRITON_VERSION-py3-igpu} UBUNTU_IMAGE=${UBUNTU_IMAGE:=ubuntu:24.04} # Set CI specific parameters -DOCKER_GPU_ARGS=${DOCKER_GPU_ARGS:-$([[ -v RUNNER_GPUS && $RUNNER_GPUS =~ ^[0-9] ]] && eval $NV_DOCKER_ARGS || echo "--runtime=nvidia -e NVIDIA_VISIBLE_DEVICES=$NVIDIA_VISIBLE_DEVICES" )} +DOCKER_GPU_ARGS=${DOCKER_GPU_ARGS:-$([[ $RUNNER_GPUS =~ ^[0-9] ]] && eval $NV_DOCKER_ARGS || echo "--gpus device=$CUDA_DEVICE" )} ############################################################################ # Check if Docker volume exists @@ -142,7 +142,6 @@ docker pull $TENSORRT_IMAGE docker run $DOCKER_GPU_ARGS \ --rm -v $DOCKER_VOLUME:/mnt \ - -e TRT_VERBOSE \ $TENSORRT_IMAGE bash -xe $VOLUME_SRCDIR/$TRT_MODEL_SCRIPT # Copy generated models to /tmp/ if not running in CI diff --git a/qa/common/gen_qa_custom_ops b/qa/common/gen_qa_custom_ops new file mode 100755 index 0000000000..304e126c34 --- /dev/null +++ b/qa/common/gen_qa_custom_ops @@ -0,0 +1,149 @@ +#!/bin/bash +# Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +############################################################################ +## This script generates custom operations needed by some of the +## Triton's CI tests. Generating these custom operations requires +## using the Pytorch container. +## +## 1. Update PYTORCH_IMAGE to match what is being +## used by the Triton release being tested. +## +## 2. Run this script to create /tmp/qa_custom_ops. +## +############################################################################ + +TRITON_VERSION=${TRITON_VERSION:=25.03} +NVIDIA_UPSTREAM_VERSION=${NVIDIA_UPSTREAM_VERSION:=$TRITON_VERSION} +PYTORCH_IMAGE=${PYTORCH_IMAGE:=nvcr.io/nvidia/pytorch:$NVIDIA_UPSTREAM_VERSION-py3} +UBUNTU_IMAGE=${UBUNTU_IMAGE:=ubuntu:24.04} + +CUDA_DEVICE=${NV_GPU:=0} + +DOCKER_GPU_ARGS=${DOCKER_GPU_ARGS:-$([[ $RUNNER_GPUS =~ ^[0-9] ]] && eval $NV_DOCKER_ARGS || echo "--gpus device=$CUDA_DEVICE" )} + +############################################################################ +# Check if Docker volume exists +############################################################################ +CI_JOB_ID=${CI_JOB_ID:=$(date +%Y%m%d_%H%M)} +DOCKER_VOLUME=${DOCKER_VOLUME:=volume.gen_qa_custom_ops.${CI_JOB_ID}} +RUNNER_ID=${RUNNER_ID:=0} +PROJECT_NAME=${PROJECT_NAME:=tritonserver} +DOCKER_VOLUME_CONTAINER=${DOCKER_VOLUME}.gen_qa_custom_ops.${CI_JOB_ID} + +if ! docker volume inspect $DOCKER_VOLUME > /dev/null 2>&1; then + echo -e "\033[34m[ INFO ] - Docker volume $DOCKER_VOLUME does not exist. Creating... \033[0m " + docker volume create $DOCKER_VOLUME --label RUNNER_ID=$RUNNER_ID --label PROJECT_NAME=$PROJECT_NAME + docker volume inspect $DOCKER_VOLUME +else + echo -e "\033[34m[ INFO ] - Docker volume in use: $DOCKER_VOLUME \033[0m " + docker volume inspect $DOCKER_VOLUME +fi + + +docker run \ + --rm \ + --label RUNNER_ID=$RUNNER_ID \ + --label PROJECT_NAME=$PROJECT_NAME \ + -v $DOCKER_VOLUME:/mnt \ + -w /mnt/$CI_JOB_ID \ + $UBUNTU_IMAGE \ + mkdir -p gen_srcdir ${TRITON_VERSION} + +docker create \ + --label RUNNER_ID=$RUNNER_ID \ + --label PROJECT_NAME=$PROJECT_NAME \ + --name $DOCKER_VOLUME_CONTAINER \ + -v $DOCKER_VOLUME:/mnt \ + -w /mnt/$CI_JOB_ID \ + $UBUNTU_IMAGE + +docker cp . $DOCKER_VOLUME_CONTAINER:/mnt/$CI_JOB_ID/gen_srcdir + +### +VOLUME_BUILD_DIR=${VOLUME_BUILD_DIR:=/mnt/$CI_JOB_ID} +VOLUME_SRCDIR=${VOLUME_SRCDIR:=$VOLUME_BUILD_DIR/gen_srcdir} +VOLUME_DESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_custom_ops + + +docker run --rm -v $DOCKER_VOLUME:/mnt -w /mnt/$CI_JOB_ID $UBUNTU_IMAGE \ +mkdir -p \ +$VOLUME_DESTDIR/libtorch_custom_ops + +PYTSCRIPT=gen.PyTorch.gen_qa_custom_ops.cmds + +# PyTorch + +cat > $PYTSCRIPT < $OPENVINOSCRIPT < /dev/null 2>&1; then + echo -e "\033[34m[ INFO ] - Docker volume $DOCKER_VOLUME does not exist. Creating... \033[0m " + docker volume create $DOCKER_VOLUME --label RUNNER_ID=$RUNNER_ID --label PROJECT_NAME=$PROJECT_NAME + docker volume inspect $DOCKER_VOLUME +else + echo -e "\033[34m[ INFO ] - Docker volume in use: $DOCKER_VOLUME \033[0m " + docker volume inspect $DOCKER_VOLUME +fi + + +docker run \ + --rm \ + --label RUNNER_ID=$RUNNER_ID \ + --label PROJECT_NAME=$PROJECT_NAME \ + -v $DOCKER_VOLUME:/mnt \ + -w /mnt/$CI_JOB_ID \ + $UBUNTU_IMAGE \ + mkdir -p gen_srcdir ${TRITON_VERSION} + +docker create \ + --label RUNNER_ID=$RUNNER_ID \ + --label PROJECT_NAME=$PROJECT_NAME \ + --name $DOCKER_VOLUME_CONTAINER \ + -v $DOCKER_VOLUME:/mnt \ + -w /mnt/$CI_JOB_ID \ + $UBUNTU_IMAGE + +docker cp . $DOCKER_VOLUME_CONTAINER:/mnt/$CI_JOB_ID/gen_srcdir + +VOLUME_BUILD_DIR=${VOLUME_BUILD_DIR:=/mnt/$CI_JOB_ID} +VOLUME_SRCDIR=${VOLUME_SRCDIR:=$VOLUME_BUILD_DIR/gen_srcdir} +VOLUME_DESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_model_repository +VOLUME_VARDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_variable_model_repository +VOLUME_IDENTITYDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_identity_model_repository +VOLUME_IDENTITYBIGDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_identity_big_model_repository +VOLUME_SHAPEDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_shapetensor_model_repository +VOLUME_RESHAPEDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_reshape_model_repository +VOLUME_SEQDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_sequence_model_repository +VOLUME_DYNASEQDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_dyna_sequence_model_repository +VOLUME_DYNASEQIMPLICITDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_dyna_sequence_implicit_model_repository +VOLUME_VARSEQDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_variable_sequence_model_repository +VOLUME_ENSEMBLEDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_ensemble_model_repository +VOLUME_NOSHAPEDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_noshape_model_repository +VOLUME_PLGDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_trt_plugin_model_repository +VOLUME_RAGGEDDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_ragged_model_repository +VOLUME_FORMATDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_trt_format_model_repository +VOLUME_DATADEPENDENTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_trt_data_dependent_model_repository +VOLUME_IMPLICITSEQDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_sequence_implicit_model_repository +VOLUME_VARIMPLICITSEQDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_variable_sequence_implicit_model_repository +VOLUME_INITIALSTATEIMPLICITSEQDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_sequence_initial_state_implicit_model_repository +VOLUME_VARINITIALSTATEIMPLICITSEQDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_variable_sequence_initial_state_implicit_model_repository +VOLUME_TORCHTRTDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/torchtrt_model_store +VOLUME_SCALARMODELSDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_scalar_models +VOLUME_IMAGEMODELSDESTDIR=$VOLUME_BUILD_DIR/$TRITON_VERSION/qa_dynamic_batch_image_model_repository + + +docker run \ + --rm \ + --label RUNNER_ID=$RUNNER_ID \ + --label PROJECT_NAME=$PROJECT_NAME \ + -v $DOCKER_VOLUME:/mnt \ + -w /mnt/$CI_JOB_ID \ + $UBUNTU_IMAGE \ + mkdir -p \ + $VOLUME_BUILD_DIR \ + $VOLUME_SRCDIR \ + $VOLUME_DESTDIR \ + $VOLUME_VARDESTDIR \ + $VOLUME_IDENTITYDESTDIR \ + $VOLUME_SIGDEFDESTDIR \ + $VOLUME_IDENTITYBIGDESTDIR \ + $VOLUME_TFPARAMETERSDESTDIR \ + $VOLUME_SHAPEDESTDIR \ + $VOLUME_RESHAPEDESTDIR \ + $VOLUME_SEQDESTDIR \ + $VOLUME_DYNASEQDESTDIR \ + $VOLUME_DYNASEQIMPLICITDESTDIR \ + $VOLUME_VARSEQDESTDIR \ + $VOLUME_ENSEMBLEDESTDIR \ + $VOLUME_NOSHAPEDESTDIR \ + $VOLUME_PLGDESTDIR \ + $VOLUME_RAGGEDDESTDIR \ + $VOLUME_FORMATDESTDIR \ + $VOLUME_DATADEPENDENTDIR \ + $VOLUME_IMPLICITSEQDESTDIR \ + $VOLUME_VARIMPLICITSEQDESTDIR \ + $VOLUME_INITIALSTATEIMPLICITSEQDESTDIR \ + $VOLUME_VARINITIALSTATEIMPLICITSEQDESTDIR \ + $VOLUME_TORCHTRTDESTDIR \ + $VOLUME_SCALARMODELSDESTDIR \ + $VOLUME_IMAGEMODELSDESTDIR + +ONNXSCRIPT=gen.ONNXRuntime.gen_qa_model_repository.cmds +OPENVINOSCRIPT=gen.OpenVINO.gen_qa_model_repository.cmds +TORCHSCRIPT=gen.PyTorch.gen_qa_model_repository.cmds +TRTSCRIPT=gen.TensorRT.gen_qa_model_repository.cmds + +# OPENVINO +# +# OpenVINO is not available on ARM so skip +if [[ "aarch64" != $(uname -m) ]] ; then + +cat > $OPENVINOSCRIPT < $ONNXSCRIPT < $ONNXSCRIPT < $TORCHSCRIPT < $TORCHSCRIPT < $TRTSCRIPT < $TRTSCRIPT < -# branch matching the runtime TRT version. If the branch is not published -# yet for this TRT version, the plugin model generation is skipped with a -# warning -- L0_trt_plugin will report missing artifacts but the rest of -# the QA model repository is still produced. -TRT_BRANCH=\$(echo \${TRT_VERSION} | cut -d . -f -2) -TRTSRC=/workspace/TensorRT -rm -rf \${TRTSRC} -if git clone --depth 1 -b release/\${TRT_BRANCH} \ - https://github.com/NVIDIA/TensorRT.git \${TRTSRC}; then - cd \${TRTSRC}/samples/python/onnx_custom_plugin && \ - rm -rf build && mkdir build && cd build && cmake .. && make -j && \ - cp libcustomHardmaxPlugin.so ${TRITON_MDLS_QA_TRT_PLUGIN_MODEL}/. - LD_PRELOAD=${TRITON_MDLS_QA_TRT_PLUGIN_MODEL}/libcustomHardmaxPlugin.so \ - python3 ${TRITON_MDLS_SRC_DIR}/gen_qa_trt_plugin_models.py \ - --models_dir=${TRITON_MDLS_QA_TRT_PLUGIN_MODEL} - chmod -R 777 ${TRITON_MDLS_QA_TRT_PLUGIN_MODEL} +python3 $VOLUME_SRCDIR/gen_qa_identity_models.py --tensorrt-shape-io --models_dir=$VOLUME_SHAPEDESTDIR +python3 $VOLUME_SRCDIR/gen_qa_sequence_models.py --tensorrt-shape-io --models_dir=$VOLUME_SHAPEDESTDIR +python3 $VOLUME_SRCDIR/gen_qa_dyna_sequence_models.py --tensorrt-shape-io --models_dir=$VOLUME_SHAPEDESTDIR +chmod -R 777 $VOLUME_SHAPEDESTDIR +python3 $VOLUME_SRCDIR/gen_qa_models.py --tensorrt --models_dir=$VOLUME_DESTDIR +chmod -R 777 $VOLUME_DESTDIR +python3 $VOLUME_SRCDIR/gen_qa_models.py --tensorrt --variable --models_dir=$VOLUME_VARDESTDIR +chmod -R 777 $VOLUME_VARDESTDIR +python3 $VOLUME_SRCDIR/gen_qa_identity_models.py --tensorrt --models_dir=$VOLUME_IDENTITYDESTDIR +python3 $VOLUME_SRCDIR/gen_qa_identity_models.py --tensorrt-compat --models_dir=$VOLUME_IDENTITYDESTDIR +chmod -R 777 $VOLUME_IDENTITYDESTDIR +python3 $VOLUME_SRCDIR/gen_qa_identity_models.py --tensorrt-big --models_dir=$VOLUME_IDENTITYBIGDESTDIR +chmod -R 777 $VOLUME_IDENTITYBIGDESTDIR +python3 $VOLUME_SRCDIR/gen_qa_reshape_models.py --tensorrt --variable --models_dir=$VOLUME_RESHAPEDESTDIR +chmod -R 777 $VOLUME_RESHAPEDESTDIR +python3 $VOLUME_SRCDIR/gen_qa_sequence_models.py --tensorrt --models_dir=$VOLUME_SEQDESTDIR +chmod -R 777 $VOLUME_SEQDESTDIR +python3 $VOLUME_SRCDIR/gen_qa_implicit_models.py --tensorrt --models_dir=$VOLUME_IMPLICITSEQDESTDIR +chmod -R 777 $VOLUME_IMPLICITSEQDESTDIR +python3 $VOLUME_SRCDIR/gen_qa_implicit_models.py --tensorrt --variable --models_dir=$VOLUME_VARIMPLICITSEQDESTDIR +chmod -R 777 $VOLUME_VARIMPLICITSEQDESTDIR +python3 $VOLUME_SRCDIR/gen_qa_dyna_sequence_models.py --tensorrt --models_dir=$VOLUME_DYNASEQDESTDIR +chmod -R 777 $VOLUME_DYNASEQDESTDIR +python3 $VOLUME_SRCDIR/gen_qa_sequence_models.py --tensorrt --variable --models_dir=$VOLUME_VARSEQDESTDIR +chmod -R 777 $VOLUME_VARSEQDESTDIR +python3 $VOLUME_SRCDIR/gen_qa_dyna_sequence_implicit_models.py --tensorrt --models_dir=$VOLUME_DYNASEQIMPLICITDESTDIR +chmod -R 777 $VOLUME_DYNASEQIMPLICITDESTDIR +python3 $VOLUME_SRCDIR/gen_qa_ragged_models.py --tensorrt --models_dir=$VOLUME_RAGGEDDESTDIR +chmod -R 777 $VOLUME_RAGGEDDESTDIR +python3 $VOLUME_SRCDIR/gen_qa_trt_format_models.py --models_dir=$VOLUME_FORMATDESTDIR +chmod -R 777 $VOLUME_FORMATDESTDIR +python3 $VOLUME_SRCDIR/gen_qa_trt_data_dependent_shape.py --models_dir=$VOLUME_DATADEPENDENTDIR +chmod -R 777 $VOLUME_DATADEPENDENTDIR +# Make shared library for custom Hardmax plugin. +if [ -d "/usr/src/tensorrt" ]; then + cd /usr/src/tensorrt/samples/python/onnx_custom_plugin else - echo "[WARNING] TensorRT release/\${TRT_BRANCH} not available on github.com/NVIDIA/TensorRT; skipping CustomHardmax plugin model generation. L0_trt_plugin coverage will be missing for this TRT version." + git clone -b release/${TENSORRT_VERSION} https://github.com/NVIDIA/TensorRT.git + cd /workspace/TensorRT/samples/python/onnx_custom_plugin fi -exit 0 +rm -rf build && mkdir build && \ +cd build && cmake .. && make -j && cp libcustomHardmaxPlugin.so $VOLUME_PLGDESTDIR/. +LD_PRELOAD=$VOLUME_PLGDESTDIR/libcustomHardmaxPlugin.so python3 $VOLUME_SRCDIR/gen_qa_trt_plugin_models.py --models_dir=$VOLUME_PLGDESTDIR +chmod -R 777 $VOLUME_PLGDESTDIR EOF - log_message.status "run: chmod a+x ${TRTSCRIPT}" - chmod a+x ${TRTSCRIPT} - if [ $? -ne 0 ]; then - log_message.error "failed: chmod ${TRTSCRIPT}" - exit 1 - fi - -} - -log_message.status "check: engine installation" -if [ "$TRITON_MODELS_USE_DOCKER" -eq 1 ] && which docker ; then - log_message.info "Docker is installed." - - define_model_output_directories - - SCRIPT_NAME_SUFFIX=docker.v2 define_models_generation_scripts - - if ! docker volume inspect $DOCKER_VOLUME > /dev/null 2>&1; then - log_message.status "docker volume: $DOCKER_VOLUME does not exist. Creating..." - docker volume create $DOCKER_VOLUME --label RUNNER_ID=$RUNNER_ID --label PROJECT_NAME=$PROJECT_NAME - log_message.status "docker volume: $DOCKER_VOLUME created" - docker volume inspect $DOCKER_VOLUME - else - log_message.status "docker volume: $DOCKER_VOLUME in use" - docker volume inspect $DOCKER_VOLUME - fi - - log_message.status "docker pull: $UBUNTU_IMAGE" - docker pull $UBUNTU_IMAGE - - log_message.status "docker volume: create destination directory on volume" - log_message.info "docker run -v $DOCKER_VOLUME:/mnt -w /mnt/$CI_JOB_ID $UBUNTU_IMAGE mkdir -p gen_srcdir ${TRITON_VERSION}" - docker run \ - --rm \ - --label RUNNER_ID=$RUNNER_ID \ - --label PROJECT_NAME=$PROJECT_NAME \ - -v $DOCKER_VOLUME:/mnt \ - -w /mnt/$CI_JOB_ID \ - $UBUNTU_IMAGE \ - mkdir -p gen_srcdir ${TRITON_VERSION} - - log_message.status "docker volume: create model directories on volume" - docker run \ - --rm \ - --label RUNNER_ID=$RUNNER_ID \ - --label PROJECT_NAME=$PROJECT_NAME \ - -v $DOCKER_VOLUME:/mnt \ - -w /mnt/$CI_JOB_ID \ - $UBUNTU_IMAGE \ - mkdir -p \ - $TRITON_MDLS_BLD_DIR \ - $TRITON_MDLS_SRC_DIR \ - $TRITON_MDLS_QA_MODEL \ - $TRITON_MDLS_QA_VARIABLE_MODEL \ - $TRITON_MDLS_QA_IDENTITY_MODEL \ - $TRITON_MDLS_QA_IDENTITY_BIG_MODEL \ - $TRITON_MDLS_QA_SHAPETENSOR_MODEL \ - $TRITON_MDLS_QA_RESHAPE_MODEL \ - $TRITON_MDLS_QA_SEQUENCE_MODEL \ - $TRITON_MDLS_QA_DYNA_SEQUENCE_MODEL \ - $TRITON_MDLS_QA_DYNA_SEQUENCE_IMPLICIT_MODEL \ - $TRITON_MDLS_QA_VARIABLE_SEQUENCE_MODEL \ - $TRITON_MDLS_QA_ENSEMBLE_MODEL \ - $TRITON_MDLS_QA_NOSHAPE_MODEL \ - $TRITON_MDLS_QA_TRT_PLUGIN_MODEL \ - $TRITON_MDLS_QA_RAGGED_MODEL \ - $TRITON_MDLS_QA_TRT_FORMAT_MODEL \ - $TRITON_MDLS_QA_TRT_DATA_DEPENDENT_MODEL \ - $TRITON_MDLS_QA_SEQUENCE_IMPLICIT_MODEL \ - $TRITON_MDLS_QA_VARIABLE_SEQUENCE_IMPLICIT_MODEL \ - $TRITON_MDLS_QA_SEQUENCE_INITIAL_STATE_IMPLICIT_MODEL \ - $TRITON_MDLS_QA_VARIABLE_SEQUENCE_INITIAL_STATE_IMPLICIT_MODEL \ - $TRITON_MDLS_QA_TORCHTRT_MODEL \ - $TRITON_MDLS_QA_SCALAR_MODELS \ - $TRITON_MDLS_QA_DYNAMIC_BATCH_IMAGE_MODEL - - log_message.status "docker container: create container $DOCKER_VOLUME_CONTAINER" - log_message.info "docker create --name $DOCKER_VOLUME_CONTAINER -v $DOCKER_VOLUME:/mnt -w /mnt/$CI_JOB_ID $UBUNTU_IMAGE" - docker create \ - --label RUNNER_ID=$RUNNER_ID \ - --label PROJECT_NAME=$PROJECT_NAME \ - --name $DOCKER_VOLUME_CONTAINER \ - -v $DOCKER_VOLUME:/mnt \ - -w /mnt/$CI_JOB_ID \ - $UBUNTU_IMAGE - - log_message.status "docker container: copy script to container" - docker cp . $DOCKER_VOLUME_CONTAINER:/mnt/$CI_JOB_ID/gen_srcdir - - if [[ "aarch64" != $(uname -m) ]] ; then - - log_message.status "docker run: $OPENVINOSCRIPT" - log_message.info "docker run $DOCKER_GPU_ARGS -v $DOCKER_VOLUME:/mnt $UBUNTU_IMAGE bash -e $TRITON_MDLS_SRC_DIR/$OPENVINOSCRIPT" - docker run \ - --rm \ - -e TRITON_GENSRCDIR=$TRITON_MDLS_SRC_DIR \ - --label RUNNER_ID=$RUNNER_ID \ - --label PROJECT_NAME=$PROJECT_NAME \ - $DOCKER_GPU_ARGS \ - -v $DOCKER_VOLUME:/mnt \ - -t \ - $UBUNTU_IMAGE \ - bash -e $TRITON_MDLS_SRC_DIR/$OPENVINOSCRIPT - - exit_code=$? - - if [ $exit_code -ne 0 ]; then - log_message.error "docker run: ${OPENVINOSCRIPT} failed" - exit 1 - fi - - rm $OPENVINOSCRIPT - fi # [[ "aarch64" != $(uname -m) ]] - - log_message.status "docker run: $ONNXSCRIPT" - log_message.info "docker run $DOCKER_GPU_ARGS -v $DOCKER_VOLUME:/mnt $UBUNTU_IMAGE bash -e $TRITON_MDLS_SRC_DIR/$ONNXSCRIPT" - docker run \ - --rm \ - -e TRITON_GENSRCDIR=$TRITON_MDLS_SRC_DIR \ - --label RUNNER_ID=$RUNNER_ID \ - --label PROJECT_NAME=$PROJECT_NAME \ - $DOCKER_GPU_ARGS \ - -v $DOCKER_VOLUME:/mnt \ - -t \ - $UBUNTU_IMAGE \ - bash -e $TRITON_MDLS_SRC_DIR/$ONNXSCRIPT - - exit_code=$? - - if [ $exit_code -ne 0 ]; then - log_message.error "docker run: ${ONNXSCRIPT} failed" - exit 1 - fi - - rm $ONNXSCRIPT - - log_message.status "docker pull: $PYTORCH_IMAGE" - log_message.info "docker pull $PYTORCH_IMAGE" - docker pull $PYTORCH_IMAGE - - log_message.status "docker run: $TORCHSCRIPT" - log_message.info "docker run $DOCKER_GPU_ARGS -v $DOCKER_VOLUME:/mnt $PYTORCH_IMAGE bash -e $TRITON_MDLS_SRC_DIR/$TORCHSCRIPT" - docker run \ - --rm \ - -e TRITON_GENSRCDIR=$TRITON_MDLS_SRC_DIR \ - --label RUNNER_ID=$RUNNER_ID \ - --label PROJECT_NAME=$PROJECT_NAME \ - $DOCKER_GPU_ARGS \ - -v $DOCKER_VOLUME:/mnt \ - -t \ - $PYTORCH_IMAGE \ - bash -e $TRITON_MDLS_SRC_DIR/$TORCHSCRIPT - - exit_code=$? - - if [ $exit_code -ne 0 ]; then - log_message.error "docker run: ${TORCHSCRIPT} failed" - exit 1 - fi - - rm $TORCHSCRIPT - - if [ "$MODEL_TYPE" != "igpu" ] ; then - log_message.status "docker pull: $TENSORRT_IMAGE" - docker pull $TENSORRT_IMAGE - - log_message.status "docker run: $TRTSCRIPT" - log_message.info "docker run $DOCKER_GPU_ARGS -v $DOCKER_VOLUME:/mnt $TENSORRT_IMAGE bash -e $TRITON_MDLS_SRC_DIR/$TRTSCRIPT" - docker run \ - --rm \ - -e TRITON_GENSRCDIR=$TRITON_MDLS_SRC_DIR \ - --label RUNNER_ID=$RUNNER_ID \ - --label PROJECT_NAME=$PROJECT_NAME \ - $DOCKER_GPU_ARGS \ - -v $DOCKER_VOLUME:/mnt \ - -t \ - -e TRT_VERBOSE \ - $TENSORRT_IMAGE \ - bash -e $TRITON_MDLS_SRC_DIR/$TRTSCRIPT - - exit_code=$? - - if [ $exit_code -ne 0 ]; then - log_message.error "docker run: ${TRTSCRIPT} failed" - exit 1 - fi - - rm $TRTSCRIPT - fi # [ "$MODEL_TYPE" != "igpu" ] - - if [ -z $CI ] ; then - log_message.status "docker cp:copying generated models to /tmp/" - docker cp $DOCKER_VOLUME_CONTAINER:$TRITON_MDLS_BLD_DIR/$TRITON_VERSION /tmp/ - log_message.status "docker rm: removing docker container $DOCKER_VOLUME_CONTAINER" - docker rm -f $(docker ps -a --filter volume=$DOCKER_VOLUME --format '{{ .ID }}') - log_message.status "docker volume rm: removing docker volume $DOCKER_VOLUME" - docker volume rm $DOCKER_VOLUME - fi # [ -z $CI ] - -elif [ "$TRITON_MODELS_USE_ENROOT" -eq 1 ] && which enroot ; then - log_message.info "NVIDIA Enroot is installed." ; - - TRITON_MDLS_BLD_DIR="/tmp/$CI_JOB_ID" define_model_output_directories - - SCRIPT_NAME_SUFFIX=enroot.v1 define_models_generation_scripts - - log_message.status "cleanup models folder if exists: $TRITON_MDLS_BLD_DIR" - rm -rf $TRITON_MDLS_BLD_DIR - - log_message.status "create models directory structure in: $TRITON_MDLS_BLD_DIR" - mkdir -p \ - $TRITON_MDLS_BLD_DIR \ - $TRITON_MDLS_SRC_DIR \ - $TRITON_MDLS_QA_MODEL \ - $TRITON_MDLS_QA_VARIABLE_MODEL \ - $TRITON_MDLS_QA_IDENTITY_MODEL \ - $TRITON_MDLS_QA_IDENTITY_BIG_MODEL \ - $TRITON_MDLS_QA_SHAPETENSOR_MODEL \ - $TRITON_MDLS_QA_RESHAPE_MODEL \ - $TRITON_MDLS_QA_SEQUENCE_MODEL \ - $TRITON_MDLS_QA_DYNA_SEQUENCE_MODEL \ - $TRITON_MDLS_QA_DYNA_SEQUENCE_IMPLICIT_MODEL \ - $TRITON_MDLS_QA_VARIABLE_SEQUENCE_MODEL \ - $TRITON_MDLS_QA_ENSEMBLE_MODEL \ - $TRITON_MDLS_QA_NOSHAPE_MODEL \ - $TRITON_MDLS_QA_TRT_PLUGIN_MODEL \ - $TRITON_MDLS_QA_RAGGED_MODEL \ - $TRITON_MDLS_QA_TRT_FORMAT_MODEL \ - $TRITON_MDLS_QA_TRT_DATA_DEPENDENT_MODEL \ - $TRITON_MDLS_QA_SEQUENCE_IMPLICIT_MODEL \ - $TRITON_MDLS_QA_VARIABLE_SEQUENCE_IMPLICIT_MODEL \ - $TRITON_MDLS_QA_SEQUENCE_INITIAL_STATE_IMPLICIT_MODEL \ - $TRITON_MDLS_QA_VARIABLE_SEQUENCE_INITIAL_STATE_IMPLICIT_MODEL \ - $TRITON_MDLS_QA_TORCHTRT_MODEL \ - $TRITON_MDLS_QA_SCALAR_MODELS \ - $TRITON_MDLS_QA_DYNAMIC_BATCH_IMAGE_MODEL - - log_message.status "copy scripts to: $TRITON_MDLS_SRC_DIR" - cp -rv $TRITON_MDLS_BASE_SCRIPT_DIR/* $TRITON_MDLS_SRC_DIR/ - - log_message.status "enroot import: $UBUNTU_IMAGE to ubuntu.$CI_JOB_ID.enroot.sqsh" - enroot import --output /tmp/ubuntu.$CI_JOB_ID.enroot.sqsh docker://$UBUNTU_IMAGE - - - log_message.status "enroot create: openvino.ubuntu.$CI_JOB_ID" - enroot create --name openvino.ubuntu.$CI_JOB_ID /tmp/ubuntu.$CI_JOB_ID.enroot.sqsh - log_message.info "enroot start: openvino.ubuntu.$CI_JOB_ID" - enroot start --root --rw -m /tmp:/tmp openvino.ubuntu.$CI_JOB_ID bash -xe $TRITON_MDLS_SRC_DIR/$OPENVINOSCRIPT - if [ $? -ne 0 ]; then - log_message.error "enroot start: ${OPENVINOSCRIPT} failed" - exit 1 - fi - - - log_message.status "enroot create: onnxruntime.ubuntu.$CI_JOB_ID" - enroot create --name onnxruntime.ubuntu.$CI_JOB_ID /tmp/ubuntu.$CI_JOB_ID.enroot.sqsh - log_message.info "enroot start: onnxruntime.ubuntu.$CI_JOB_ID" - enroot start --root --rw -m /tmp:/tmp onnxruntime.ubuntu.$CI_JOB_ID bash -xe $TRITON_MDLS_SRC_DIR/$ONNXSCRIPT - if [ $? -ne 0 ]; then - log_message.error "enroot start: ${ONNXSCRIPT} failed" - exit 1 - fi - - - log_message.status "enroot import: $PYTORCH_IMAGE to /tmp/pytorch.$CI_JOB_ID.enroot.sqsh" - enroot import --output /tmp/pytorch.$CI_JOB_ID.enroot.sqsh docker://$PYTORCH_IMAGE - if [ $? -ne 0 ]; then - log_message.error "enroot import: ${PYTORCH_IMAGE} failed" - exit 1 - fi - - log_message.status "enroot create: pytorch.$CI_JOB_ID" - enroot create --name pytorch.$CI_JOB_ID /tmp/pytorch.$CI_JOB_ID.enroot.sqsh - log_message.info "enroot start: pytorch.$CI_JOB_ID" - enroot start --rw -m /tmp:/tmp pytorch.$CI_JOB_ID bash -xe $TRITON_MDLS_SRC_DIR/$TORCHSCRIPT - if [ $? -ne 0 ]; then - log_message.error "enroot start: ${TORCHSCRIPT} failed" - exit 1 - fi - - log_message.status "enroot import: $TENSORRT_IMAGE to /tmp/tensorrt.$CI_JOB_ID.enroot.sqsh" - enroot import --output /tmp/tensorrt.$CI_JOB_ID.enroot.sqsh docker://$TENSORRT_IMAGE - log_message.status "enroot create: tensorrt.$CI_JOB_ID" - enroot create --name tensorrt.$CI_JOB_ID /tmp/tensorrt.$CI_JOB_ID.enroot.sqsh - log_message.info "enroot start: tensorrt.$CI_JOB_ID" - enroot start --rw -m /tmp:/tmp tensorrt.$CI_JOB_ID bash -xe $TRITON_MDLS_SRC_DIR/$TRTSCRIPT - if [ $? -ne 0 ]; then - log_message.error "enroot start: ${TRTSCRIPT} failed" - exit 1 - fi +chmod a+x $TRTSCRIPT +if [ $? -ne 0 ]; then + echo -e "Failed: chmod" + exit 1 +fi + +if [ "$MODEL_TYPE" != "igpu" ] ; then + docker cp $TRTSCRIPT $DOCKER_VOLUME_CONTAINER:$VOLUME_SRCDIR + docker pull $TENSORRT_IMAGE + + echo -e "\033[34m[ INFO ] - Running: $TRTSCRIPT \033[0m " + + docker run \ + --rm \ + --label RUNNER_ID=$RUNNER_ID \ + --label PROJECT_NAME=$PROJECT_NAME \ + $DOCKER_GPU_ARGS \ + -v $DOCKER_VOLUME:/mnt \ + $TENSORRT_IMAGE \ + bash -xe $VOLUME_SRCDIR/$TRTSCRIPT + + if [ $? -ne 0 ]; then + echo -e "Failed" + exit 1 + fi -else - log_message.warning "Neither Docker nor NVIDIA Enroot is installed." ; - log_message.warning "Please install Docker or NVIDIA Enroot to generate the models." ; fi + +if [ -z $CI ] ; then + echo -e "\033[34m[ INFO ] - Copying generated models to /tmp/ \033[0m " + docker cp $DOCKER_VOLUME_CONTAINER:$VOLUME_BUILD_DIR/$TRITON_VERSION /tmp/ + echo -e "\033[34m[ INFO ] - Removing Docker container $DOCKER_VOLUME_CONTAINER \033[0m " + docker rm -f $(docker ps -a --filter volume=$DOCKER_VOLUME --format '{{ .ID }}') + echo -e "\033[34m[ INFO ] - Removing Docker volume $DOCKER_VOLUME \033[0m " + docker volume rm $DOCKER_VOLUME +fi \ No newline at end of file diff --git a/qa/common/gen_qa_models.py b/qa/common/gen_qa_models.py index d305f07ce3..cd7efea723 100755 --- a/qa/common/gen_qa_models.py +++ b/qa/common/gen_qa_models.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -28,7 +28,6 @@ import argparse import os -import sys from builtins import range import gen_ensemble_model_utils as emu @@ -46,14 +45,6 @@ np_dtype_string = np.dtype(object) from typing import List, Tuple -_color_blue = "\033[94m" -_color_cyan = "\033[36m" -_color_green = "\033[32m" -_color_magenta = "\033[35m" -_color_red = "\033[31m" -_color_reset = "\033[0m" -_color_yellow = "\033[33m" - def create_plan_dynamic_rf_modelfile( models_dir, @@ -75,11 +66,7 @@ def create_plan_dynamic_rf_modelfile( trt_memory_format = trt.TensorFormat.LINEAR # Create the model - TRT_LOGGER = ( - trt.Logger(trt.Logger.INFO) - if os.environ.get("TRT_VERBOSE") != "1" - else trt.Logger(trt.Logger.VERBOSE) - ) + TRT_LOGGER = trt.Logger(trt.Logger.INFO) builder = trt.Builder(TRT_LOGGER) network = builder.create_network() if max_batch == 0: @@ -160,8 +147,7 @@ def create_plan_dynamic_rf_modelfile( profile.set_shape("INPUT1", min_shape, opt_shape, max_shape) flags = 1 << int(trt.BuilderFlag.PREFER_PRECISION_CONSTRAINTS) - if hasattr(trt.BuilderFlag, "REJECT_EMPTY_ALGORITHMS"): - flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) + flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) datatype_set = set([trt_input_dtype, trt_output0_dtype, trt_output1_dtype]) for dt in datatype_set: @@ -194,7 +180,7 @@ def create_plan_dynamic_rf_modelfile( try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(model_version_dir + "/model.plan", "wb") as f: @@ -220,11 +206,7 @@ def create_plan_dynamic_modelfile( trt_output1_dtype = np_to_trt_dtype(output1_dtype) # Create the model - TRT_LOGGER = ( - trt.Logger(trt.Logger.INFO) - if os.environ.get("TRT_VERBOSE") != "1" - else trt.Logger(trt.Logger.VERBOSE) - ) + TRT_LOGGER = trt.Logger(trt.Logger.INFO) builder = trt.Builder(TRT_LOGGER) network = builder.create_network() if max_batch == 0: @@ -358,7 +340,6 @@ def create_plan_dynamic_modelfile( output0_dtype, output1_dtype, ) - print(f"{_color_green}Creating model {model_name}{_color_reset}") if min_dim != 1 or max_dim != 32: model_name = "{}-{}-{}".format(model_name, min_dim, max_dim) @@ -366,7 +347,7 @@ def create_plan_dynamic_modelfile( try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(model_version_dir + "/model.plan", "wb") as f: @@ -391,11 +372,7 @@ def create_plan_fixed_rf_modelfile( trt_memory_format = trt.TensorFormat.LINEAR # Create the model - TRT_LOGGER = ( - trt.Logger(trt.Logger.INFO) - if os.environ.get("TRT_VERBOSE") != "1" - else trt.Logger(trt.Logger.VERBOSE) - ) + TRT_LOGGER = trt.Logger(trt.Logger.INFO) builder = trt.Builder(TRT_LOGGER) network = builder.create_network() if max_batch == 0: @@ -451,8 +428,7 @@ def create_plan_fixed_rf_modelfile( profile.set_shape("INPUT1", min_shape, opt_shape, max_shape) flags = 1 << int(trt.BuilderFlag.PREFER_PRECISION_CONSTRAINTS) - if hasattr(trt.BuilderFlag, "REJECT_EMPTY_ALGORITHMS"): - flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) + flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) datatype_set = set([trt_input_dtype, trt_output0_dtype, trt_output1_dtype]) for dt in datatype_set: @@ -479,12 +455,11 @@ def create_plan_fixed_rf_modelfile( output0_dtype, output1_dtype, ) - print(f"{_color_green}Creating model {model_name}{_color_reset}") model_version_dir = models_dir + "/" + model_name + "/" + str(model_version) try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(model_version_dir + "/model.plan", "wb") as f: @@ -508,11 +483,7 @@ def create_plan_fixed_modelfile( trt_output1_dtype = np_to_trt_dtype(output1_dtype) # Create the model - TRT_LOGGER = ( - trt.Logger(trt.Logger.INFO) - if os.environ.get("TRT_VERBOSE") != "1" - else trt.Logger(trt.Logger.VERBOSE) - ) + TRT_LOGGER = trt.Logger(trt.Logger.INFO) builder = trt.Builder(TRT_LOGGER) network = builder.create_network() if max_batch == 0: @@ -567,12 +538,11 @@ def create_plan_fixed_modelfile( output0_dtype, output1_dtype, ) - print(f"{_color_green}Creating model {model_name}{_color_reset}") model_version_dir = models_dir + "/" + model_name + "/" + str(model_version) try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(model_version_dir + "/model.plan", "wb") as f: @@ -597,6 +567,9 @@ def create_plan_modelfile( input_dtype, output0_dtype, output1_dtype, + input_shape, + output0_shape, + output1_shape, ): return @@ -713,6 +686,9 @@ def create_plan_modelconfig( input_dtype, output0_dtype, output1_dtype, + input_shape, + output0_shape, + output1_shape, ): return @@ -734,7 +710,6 @@ def create_plan_modelconfig( output0_dtype, output1_dtype, ) - print(f"{_color_green}Creating config for {model_name}{_color_reset}") if min_dim != 1 or max_dim != 32: model_name = "{}-{}-{}".format(model_name, min_dim, max_dim) @@ -840,7 +815,7 @@ def create_plan_modelconfig( try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(config_dir + "/config.pbtxt", "w") as cfile: @@ -867,6 +842,9 @@ def create_onnx_modelfile( input_dtype, output0_dtype, output1_dtype, + input_shape, + output0_shape, + output1_shape, ): return @@ -885,7 +863,6 @@ def create_onnx_modelfile( output0_dtype, output1_dtype, ) - print(f"{_color_green}Creating model {model_name}{_color_reset}") model_version_dir = models_dir + "/" + model_name + "/" + str(model_version) batch_dim = [] if max_batch == 0 else [None] @@ -955,7 +932,7 @@ def create_onnx_modelfile( try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir onnx.save(model_def, model_version_dir + "/model.onnx") @@ -964,6 +941,7 @@ def create_onnx_modelfile( def create_onnx_modelconfig( models_dir, max_batch, + model_version, input_shape, output0_shape, output1_shape, @@ -977,6 +955,9 @@ def create_onnx_modelconfig( input_dtype, output0_dtype, output1_dtype, + input_shape, + output0_shape, + output1_shape, ): return @@ -987,9 +968,6 @@ def create_onnx_modelconfig( output0_dtype, output1_dtype, ) - - print(f"{_color_green}Creating config for {model_name}{_color_reset}") - config_dir = models_dir + "/" + model_name # [TODO] move create_general_modelconfig() out of emu as it is general @@ -1011,15 +989,15 @@ def create_onnx_modelconfig( try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir - with open(config_dir + "/config.pbtxt", "w") as file: - file.write(config) + with open(config_dir + "/config.pbtxt", "w") as cfile: + cfile.write(config) - with open(config_dir + "/output0_labels.txt", "w") as file: + with open(config_dir + "/output0_labels.txt", "w") as lfile: for l in range(output0_label_cnt): - file.write("label" + str(l) + "\n") + lfile.write("label" + str(l) + "\n") def create_libtorch_modelfile( @@ -1054,9 +1032,6 @@ def create_libtorch_modelfile( output0_dtype, output1_dtype, ) - - print(f"{_color_green}Creating model {model_name}{_color_reset}") - # handle for -1 (when variable) since can't create tensor with shape of [-1] input_shape = [abs(ips) for ips in input_shape] @@ -1282,367 +1257,20 @@ def forward(self, INPUT0, INPUT1): addSubModel = AddSubNet((torch_output0_dtype, torch_output1_dtype, swap)) traced = torch.jit.script(addSubModel) - model_version_dir = f"{models_dir}/{model_name}/{model_version}" - - try: - os.makedirs(model_version_dir) - except OSError: - pass # ignore existing dir - - traced.save(f"{model_version_dir}/model.pt") - - -def generate_torch_aoti_sample_inputs( - input_shape, - input_dtype, - device, -): - # handle for -1 (when variable) since can't create tensor with shape of [-1] - input_shape = [abs(ips) for ips in input_shape] - - np_to_torch_dtype = { - np.int8: torch.int8, - np.int16: torch.int16, - np.int32: torch.int32, - np.int64: torch.int64, - np.float16: torch.float16, - np.float32: torch.float32, - np.float64: torch.float64, - np.uint8: torch.uint8, - np.uint16: torch.uint16, - np.uint32: torch.uint32, - np.uint64: torch.uint64, - } - - if input_dtype not in np_to_torch_dtype: - print( - f"{_color_yellow}warning: dtype {input_dtype} is unsupported; falling back to torch.int32{_color_reset}" - ) - input_dtype = np.int32 - - input0 = torch.zeros( - input_shape, dtype=np_to_torch_dtype[input_dtype], device=device - ) - input1 = torch.zeros( - input_shape, dtype=np_to_torch_dtype[input_dtype], device=device - ) - - return (input0, input1) - - -def np_to_dtype(np_dtype): - if np_dtype == np.int8: - return torch.int8 - elif np_dtype == np.int16: - return torch.int16 - elif np_dtype == np.int32: - return torch.int32 - elif np_dtype == np.int64: - return torch.int64 - elif np_dtype == np.float16: - return torch.float16 - elif np_dtype == np.float32: - return torch.float32 - elif np_dtype == np.float64: - return torch.float64 - elif np_dtype == np.uint8: - return torch.uint8 - elif np_dtype == np.uint16: - return torch.uint16 - elif np_dtype == np.uint32: - return torch.uint32 - elif np_dtype == np.uint64: - return torch.uint64 - else: - print( - f"{_color_yellow}warning: dtype {np_dtype} is unsupported; falling back to torch.int32{_color_reset}" - ) - return torch.int32 - - -def create_torch_aoti_model_file( - models_dir, - model_version, - input_shape, - input_dtype, - output_dtype, - swap=False, -): - model_name = tu.get_model_name( - "torch_aoti", - input_dtype, - output_dtype, - None, - ) - - # AOTI compiles tensor operations and does not support string (object) dtype - if input_dtype == np_dtype_string or output_dtype == np_dtype_string: - print( - f"{_color_yellow}warning: Skipping AOTI model {model_name}: " - f"string/object dtype is not supported for AOTI compilation{_color_reset}" - ) - return False - - model_version_dir = os.path.join(models_dir, model_name, str(model_version)) - - print(f"{_color_green}Creating model {model_name}{_color_reset}") - - torch_input_dtype: torch.dtype = np_to_dtype(input_dtype) - torch_output_dtype: torch.dtype = np_to_dtype(output_dtype) - - print(f"{model_name}({torch_input_dtype}) -> {torch_output_dtype}") - - # handle for -1 (when variable) since can't create tensor with shape of [-1] - input_shape = [abs(ips) for ips in input_shape] - - try: - os.makedirs(model_version_dir) - except OSError: - pass # ignore existing dir - - class AddSubNet(nn.Module): - def __init__( - self, - swap: bool, - input_dtype: torch.dtype, - output_dtype: torch.dtype, - ) -> None: - self.swap = swap - self.input_dtype = input_dtype - self.output_dtype = output_dtype - super(AddSubNet, self).__init__() - - def forward(self, INPUT0: torch.Tensor, INPUT1: torch.Tensor) -> torch.Tensor: - if INPUT0.dtype != self.input_dtype: - raise TypeError( - f"INPUT0 expected {self.input_dtype} vs. actual {INPUT0.dtype} type." - ) - if INPUT1.dtype != self.input_dtype: - raise TypeError( - f"INPUT1 expected {self.input_dtype} vs. actual {INPUT1.dtype} type." - ) - return (INPUT0 - INPUT1 if self.swap else INPUT0 + INPUT1).to( - self.output_dtype, - ) - - device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - model = AddSubNet(swap, torch_input_dtype, torch_output_dtype) - model.to(device) - model = model.eval() - - sample_inputs = generate_torch_aoti_sample_inputs(input_shape, input_dtype, device) - package_path = os.path.join(model_version_dir, "model.pt2") - - try: - exported_model = torch.export.export(model, sample_inputs) - torch._inductor.aoti_compile_and_package( - exported_model, - package_path=package_path, - ) - except Exception as e: - print( - f"{_color_red}error: Failed to create model {model_name}{_color_reset}", - file=sys.stderr, - ) - print(f"\n{_color_red}{e}{_color_reset}\n", file=sys.stderr) - return False - - return True - - -def create_torch_aoti_complex_model_file( - models_dir: str, -): - base_name = "torch_aoti_complex" - model_names = [ - f"{base_name}_named", - f"{base_name}_index", - ] - model_version_dirs = [ - os.path.join(models_dir, model_names[0], "1"), - os.path.join(models_dir, model_names[1], "1"), - ] - - for model_version_dir in model_version_dirs: - try: - os.makedirs(model_version_dir) - except OSError: - pass # ignore existing dir - - print(f"{_color_green}Creating model {base_name}{_color_reset}") - - class TorchAotiComplex(torch.nn.Module): - def __init__(self): - super().__init__() - - def forward( - self, - hdata: torch.Tensor, - vdata: torch.Tensor, - options: dict[str, torch.Tensor], - ) -> dict[ - str, - torch.Tensor | tuple[torch.Tensor, torch.Tensor] | dict[str, torch.Tensor], - ]: - out = { - "AAA": hdata + vdata, - "ZZZ": hdata - vdata, - "BBB": ( - hdata, - vdata, - ), - "CCC": options, - } - - return out - - device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - model = TorchAotiComplex() - model.to(device) - model = model.eval() - - SHAPE = (1, 16) - - sample_args = ( - torch.zeros(SHAPE, dtype=torch.int8, device=device), - torch.zeros(SHAPE, dtype=torch.int8, device=device), - { - "option1": torch.zeros(SHAPE, dtype=torch.int8, device=device), - "option2": torch.zeros(SHAPE, dtype=torch.int8, device=device), - }, - ) - - # Export and package the model - print(f"{_color_green}Exporting and packaging the model...{_color_reset}") - - model_file_name = "model.pt2" - package_paths = [ - os.path.join(model_version_dirs[0], model_file_name), - os.path.join(model_version_dirs[1], model_file_name), - ] - - try: - exported_model = torch.export.export(model, sample_args) - torch._inductor.aoti_compile_and_package( - exported_model, - package_path=package_paths[0], - ) - except Exception as e: - print( - f"{_color_red}error: Failed to create model {base_name}{_color_reset}", - file=sys.stderr, - ) - print(f"\n{_color_red}{e}{_color_reset}\n", file=sys.stderr) - return False - - try: - # Now load and run the packaged model - print(f"{_color_cyan}Loading and running the packaged model...{_color_reset}") - - compiled_model = torch._inductor.aoti_load_package(package_paths[0]) - - print(f"{_color_cyan}Compiled model call spec:{_color_reset}") - - for elem in compiled_model.loader.get_call_spec(): - print(elem) - - print(f"{_color_cyan}Running the compiled model...{_color_reset}") - - with torch.inference_mode(): - hdata = torch.randint( - low=0, - high=127, - size=SHAPE, - dtype=torch.int8, - device=device, - ) - vdata = torch.randint( - low=0, - high=127, - size=SHAPE, - dtype=torch.int8, - device=device, - ) - options = { - "option1": torch.randint( - low=0, - high=127, - size=SHAPE, - dtype=torch.int8, - device=device, - ), - "option2": torch.randint( - low=0, - high=127, - size=SHAPE, - dtype=torch.int8, - device=device, - ), - } - - _ = compiled_model(hdata, vdata, options) - - print( - f'{_color_green}Model "{base_name}" successfully executed.{_color_reset}' - ) - except Exception as e: - print( - f"{_color_red}error: Failed to validate model {base_name}{_color_reset}", - file=sys.stderr, - ) - print(f"\n{_color_red}{e}{_color_reset}\n", file=sys.stderr) - return False - - # Copy the compiled model package to the alternate model folder. - # Both the named and ordinal addressing versions of the model (from Triton's point-of-view) use the same compiled model. - shutil.copy(package_paths[0], package_paths[1]) - - return True - - -def create_torchvision_aoti_model_file( - models_dir: str, - max_batch: int, -): - model_name = "torchvision_aoti" - model_version_dir = os.path.join(models_dir, model_name, "1") + model_version_dir = models_dir + "/" + model_name + "/" + str(model_version) try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir - print(f"{_color_green}Creating model {model_name}{_color_reset}") - - device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - model = models.resnet50(weights=models.ResNet50_Weights.DEFAULT) - model = model.to(device) - model = model.eval() - - SHAPE = (max_batch, 3, 224, 224) - - # Example input tensor with batch size 1 and 3 color channels (RGB), height and width of 224 - sample_inputs = (torch.zeros(SHAPE, dtype=torch.float32, device=device),) - - package_path = os.path.join(model_version_dir, "model.pt2") - - try: - ep = torch.export.export(model, sample_inputs) - torch._inductor.aoti_compile_and_package(ep, package_path=package_path) - except Exception as e: - print( - f"{_color_red}error: Failed to create model {model_name}{_color_reset}", - file=sys.stderr, - ) - print(f"\n{_color_red}{e}{_color_reset}\n", file=sys.stderr) - return False - - return True + traced.save(model_version_dir + "/model.pt") def create_libtorch_modelconfig( models_dir, max_batch, + model_version, input_shape, output0_shape, output1_shape, @@ -1668,9 +1296,9 @@ def create_libtorch_modelconfig( if version_policy is not None: type, val = version_policy if type == "latest": - version_policy_str = f"{{ latest {{ num_versions: {val} }} }}" + version_policy_str = "{{ latest {{ num_versions: {} }}}}".format(val) elif type == "specific": - version_policy_str = f"{{ specific {{ versions: {val} }} }}" + version_policy_str = "{{ specific {{ versions: {} }}}}".format(val) else: version_policy_str = "{ all { }}" @@ -1681,342 +1309,62 @@ def create_libtorch_modelconfig( output0_dtype, output1_dtype, ) - - print(f"{_color_green}Creating config for {model_name}{_color_reset}") - - label_filename = "output0_labels.txt" - config_dir = f"{models_dir}/{model_name}" - config = f""" -backend: "pytorch" -name: "{model_name}" + config_dir = models_dir + "/" + model_name + config = """ +name: "{}" platform: "pytorch_libtorch" -max_batch_size: {max_batch} -version_policy: {version_policy_str} +max_batch_size: {} +version_policy: {} input [ {{ name: "INPUT0" - data_type: {np_to_model_dtype(input_dtype)} - dims: [ {tu.shape_to_dims_str(input_shape)} ] + data_type: {} + dims: [ {} ] }}, {{ name: "INPUT1" - data_type: {np_to_model_dtype(input_dtype)} - dims: [ {tu.shape_to_dims_str(input_shape)} ] + data_type: {} + dims: [ {} ] }} ] output [ {{ name: "OUTPUT__0" - data_type: {np_to_model_dtype(output0_dtype)} - dims: [ {tu.shape_to_dims_str(output0_shape)} ] - label_filename: "{label_filename}" + data_type: {} + dims: [ {} ] + label_filename: "output0_labels.txt" }}, {{ name: "OUTPUT__1" - data_type: {np_to_model_dtype(output1_dtype)} - dims: [ {tu.shape_to_dims_str(output1_shape)} ] + data_type: {} + dims: [ {} ] }} ] -""" - - try: - os.makedirs(config_dir) - except OSError: - pass # ignore existing dir - - config_path = os.path.join(config_dir, "config.pbtxt") - - with open(config_path, "w") as file: - file.write(config) - print(f"Created {config_path}") - - with open(f"{config_dir}/{label_filename}", "w") as file: - for l in range(output0_label_cnt): - file.write("label" + str(l) + "\n") - print(f"Created {config_dir}/{label_filename}") - - -def create_torch_aoti_model_config( - models_dir, - input_shape, - output_shape, - input_dtype, - output_dtype, - output_label_cnt, - version_policy, -): - # Unpack version policy - version_policy_str = "{ latest { num_versions: 1 }}" - if version_policy is not None: - type, val = version_policy - if type == "latest": - version_policy_str = f"{{ latest {{ num_versions: {val} }} }}" - elif type == "specific": - version_policy_str = f"{{ specific {{ versions: {val} }} }}" - else: - version_policy_str = "{ all { }}" - - # Use a different model name for the non-batching variant - model_name = tu.get_model_name( - "torch_aoti", - input_dtype, - output_dtype, - None, +""".format( + model_name, + max_batch, + version_policy_str, + np_to_model_dtype(input_dtype), + tu.shape_to_dims_str(input_shape), + np_to_model_dtype(input_dtype), + tu.shape_to_dims_str(input_shape), + np_to_model_dtype(output0_dtype), + tu.shape_to_dims_str(output0_shape), + np_to_model_dtype(output1_dtype), + tu.shape_to_dims_str(output1_shape), ) - print(f"{_color_green}Creating config for {model_name}{_color_reset}") - - label_filename = "output_labels.txt" - config_dir = os.path.join(models_dir, model_name) - config = f""" -backend: "pytorch" -name: "{model_name}" -platform: "torch_aoti" -version_policy: {version_policy_str} -input [ - {{ - name: "ARGS[0]" - data_type: {np_to_model_dtype(input_dtype)} - dims: [ {tu.shape_to_dims_str(input_shape)} ] - }}, - {{ - name: "ARGS[1]" - data_type: {np_to_model_dtype(input_dtype)} - dims: [ {tu.shape_to_dims_str(input_shape)} ] - }} -] -output [ - {{ - name: "RESULT" - data_type: {np_to_model_dtype(output_dtype)} - dims: [ {tu.shape_to_dims_str(output_shape)} ] - label_filename: "{label_filename}" - }} -] -instance_group [{{ kind: {"KIND_GPU" if torch.cuda.is_available() else "KIND_CPU"} }}] -""" - - try: - os.makedirs(config_dir) - except OSError: - pass # ignore existing dir - - config_path = os.path.join(config_dir, "config.pbtxt") - - with open(config_path, "w") as file: - file.write(config) - print(f"Created {config_path}") - - label_path = os.path.join(config_dir, label_filename) - - with open(label_path, "w") as file: - for l in range(output_label_cnt): - file.write(f"label{l}\n") - print(f"Created {label_path}") - - -def create_torch_aoti_complex_model_config( - models_dir, -): - base_name = "torch_aoti_complex" - model_names = [ - f"{base_name}_named", - f"{base_name}_index", - ] - - print(f"{_color_green}Creating config for {base_name}{_color_reset}") - - config_dirs = [ - os.path.join(models_dir, model_names[0]), - os.path.join(models_dir, model_names[1]), - ] - configs = [ - f""" -backend: "pytorch" -platform: "torch_aoti" -name: "{model_names[0]}" -input: [ - {{ - name: "ARGS[0]" - data_type: TYPE_INT8 - dims: [1, 16] - }}, - {{ - name: "ARGS[1]" - data_type: TYPE_INT8 - dims: [1, 16] - }}, - {{ - name: "ARGS[2][option1]" - data_type: TYPE_INT8 - dims: [1, 16] - }}, - {{ - name: "ARGS[2][option2]" - data_type: TYPE_INT8 - dims: [1, 16] - }} -] -output: [ - {{ - name: "RESULT[AAA]" - data_type: TYPE_INT8 - dims: [1, 16] - }}, - {{ - name: "RESULT[BBB][0]" - data_type: TYPE_INT8 - dims: [1, 16] - }}, - {{ - name: "RESULT[BBB][1]" - data_type: TYPE_INT8 - dims: [1, 16] - }}, - {{ - name: "RESULT[CCC][option1]" - data_type: TYPE_INT8 - dims: [1, 16] - }}, - {{ - name: "RESULT[CCC][option2]" - data_type: TYPE_INT8 - dims: [1, 16] - }}, - {{ - name: "RESULT[ZZZ]" - data_type: TYPE_INT8 - dims: [1, 16] - }} -] -instance_group [{{ kind: {"KIND_GPU" if torch.cuda.is_available() else "KIND_CPU"} }}] -""", - f""" -backend: "pytorch" -name: "{model_names[1]}" -platform: "torch_aoti" -input: [ - {{ - name: "INPUT__0" - data_type: TYPE_INT8 - dims: [1, 16] - }}, - {{ - name: "INPUT__1" - data_type: TYPE_INT8 - dims: [1, 16] - }}, - {{ - name: "INPUT__2" - data_type: TYPE_INT8 - dims: [1, 16] - }}, - {{ - name: "INPUT__3" - data_type: TYPE_INT8 - dims: [1, 16] - }} -] -output: [ - {{ - name: "OUTPUT__0" - data_type: TYPE_INT8 - dims: [1, 16] - }}, - {{ - name: "OUTPUT__1" - data_type: TYPE_INT8 - dims: [1, 16] - }}, - {{ - name: "OUTPUT__2" - data_type: TYPE_INT8 - dims: [1, 16] - }}, - {{ - name: "OUTPUT__3" - data_type: TYPE_INT8 - dims: [1, 16] - }}, - {{ - name: "OUTPUT__4" - data_type: TYPE_INT8 - dims: [1, 16] - }}, - {{ - name: "OUTPUT__5" - data_type: TYPE_INT8 - dims: [1, 16] - }} -] -instance_group [{{ kind: {"KIND_GPU" if torch.cuda.is_available() else "KIND_CPU"} }}] -""", - ] - - for i in range(2): - config_dir = config_dirs[i] - try: - os.makedirs(config_dir) - except OSError: - pass # ignore existing dir - - config_path = os.path.join(config_dir, "config.pbtxt") - - with open(config_path, "w") as file: - file.write(configs[i]) - print(f"Created {config_path}") - - -def create_torchvision_aoti_model_config( - models_dir: str, - max_batch: int, -): - model_name = "torchvision_aoti" - label_filename = "resnet50_labels.txt" - - print(f"{_color_green}Creating config for {model_name}{_color_reset}") - - config_dir = os.path.join(models_dir, model_name) - config = f""" -backend: "pytorch" -name: "{model_name}" -platform: "torch_aoti" -max_batch_size: {max_batch} -input [ - {{ - name: "ARGS[0]" - data_type: TYPE_FP32 - dims: [ 3, 224, 224 ] - }}] -output [ - {{ - name: "RESULT" - data_type: TYPE_FP32 - dims: [ 1000 ] - label_filename: "{label_filename}" - }} -] -instance_group [{{ kind: {"KIND_GPU" if torch.cuda.is_available() else "KIND_CPU"} }}] -""" - try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir - config_path = os.path.join(config_dir, "config.pbtxt") - - with open(config_path, "w") as file: - file.write(config) - print(f"Created {config_path}") - - source_path = os.environ.get("TRITON_GENSRCDIR", default="gen_srcdir") - source_filename = os.path.join(source_path, RESNET50_LABEL_FILE) - - target_path = os.path.join(config_dir, label_filename) + with open(config_dir + "/config.pbtxt", "w") as cfile: + cfile.write(config) - shutil.copyfile(source_filename, target_path) - print(f"Created {target_path}") + with open(config_dir + "/output0_labels.txt", "w") as lfile: + for l in range(output0_label_cnt): + lfile.write("label" + str(l) + "\n") def create_openvino_modelfile( @@ -2037,6 +1385,8 @@ def create_openvino_modelfile( output0_dtype, output1_dtype, batch_dim + input_shape, + batch_dim + output0_shape, + batch_dim + output1_shape, ): return @@ -2047,7 +1397,6 @@ def create_openvino_modelfile( output0_dtype, output1_dtype, ) - print(f"{_color_green}Creating model {model_name}{_color_reset}") model_version_dir = models_dir + "/" + model_name + "/" + str(model_version) in0 = ov.opset1.parameter( @@ -2073,6 +1422,7 @@ def create_openvino_modelfile( def create_openvino_modelconfig( models_dir, max_batch, + model_version, input_shape, output0_shape, output1_shape, @@ -2088,6 +1438,8 @@ def create_openvino_modelconfig( output0_dtype, output1_dtype, batch_dim + input_shape, + batch_dim + output0_shape, + batch_dim + output1_shape, ): return @@ -2109,7 +1461,6 @@ def create_openvino_modelconfig( output0_dtype, output1_dtype, ) - print(f"{_color_green}Creating config for {model_name}{_color_reset}") config_dir = models_dir + "/" + model_name # platform is empty and backend is 'openvino' for openvino model @@ -2159,7 +1510,7 @@ def create_openvino_modelconfig( try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(config_dir + "/config.pbtxt", "w") as cfile: @@ -2181,10 +1532,8 @@ def create_models( output0_label_cnt, version_policy=None, ): - print(f"{_color_blue}Creating models in {models_dir}{_color_reset}") model_version = 1 if FLAGS.tensorrt: - print(f"{_color_magenta}TensorRT model generation requested{_color_reset}") # max-batch 8 suffix = () if ( @@ -2275,11 +1624,11 @@ def create_models( ) if FLAGS.onnx: - print(f"{_color_magenta}ONNX model generation requested{_color_reset}") # max-batch 8 create_onnx_modelconfig( models_dir, 8, + model_version, input_shape, output0_shape, output1_shape, @@ -2304,6 +1653,7 @@ def create_models( create_onnx_modelconfig( models_dir, 0, + model_version, input_shape, output0_shape, output1_shape, @@ -2326,11 +1676,11 @@ def create_models( ) if FLAGS.libtorch: - print(f"{_color_magenta}PyTorch: PT model generation requested{_color_reset}") # max-batch 8 create_libtorch_modelconfig( models_dir, 8, + model_version, input_shape, output0_shape, output1_shape, @@ -2355,6 +1705,7 @@ def create_models( create_libtorch_modelconfig( models_dir, 0, + model_version, input_shape, output0_shape, output1_shape, @@ -2376,35 +1727,12 @@ def create_models( output1_dtype, ) - if FLAGS.torch_aoti: - if output0_dtype == output1_dtype: - print( - f"{_color_magenta}PyTorch: AOTI model generation requested{_color_reset}" - ) - # max-batch 8 - if create_torch_aoti_model_file( - models_dir, - model_version, - input_shape, - input_dtype, - output0_dtype, - ): - create_torch_aoti_model_config( - models_dir, - input_shape, - output0_shape, - input_dtype, - output0_dtype, - output0_label_cnt, - version_policy, - ) - if FLAGS.openvino: - print(f"{_color_magenta}OpenVINO model generation requested{_color_reset}") # max-batch 8 create_openvino_modelconfig( models_dir, 8, + model_version, input_shape, output0_shape, output1_shape, @@ -2429,6 +1757,7 @@ def create_models( create_openvino_modelconfig( models_dir, 0, + model_version, input_shape, output0_shape, output1_shape, @@ -2451,7 +1780,6 @@ def create_models( ) if FLAGS.ensemble: - print(f"{_color_magenta}Ensemble model generation requested{_color_reset}") for pair in emu.platform_types_and_validation(): if not pair[1]( input_dtype, @@ -2589,18 +1917,6 @@ def create_fixed_models( action="store_true", help="Generate Pytorch LibTorch models", ) - parser.add_argument( - "--torch-aoti", - required=False, - action="store_true", - help="Generate Pytorch LibTorch models using PT2", - ) - parser.add_argument( - "--torchvision-aoti", - required=False, - action="store_true", - help="Generate Pytorch Torchvision models using PT2", - ) parser.add_argument( "--openvino", required=False, @@ -2627,18 +1943,9 @@ def create_fixed_models( import tensorrt as trt if FLAGS.onnx: import onnx - if FLAGS.libtorch or FLAGS.torch_aoti: - import shutil - + if FLAGS.libtorch: import torch from torch import nn - if FLAGS.torchvision_aoti: - import shutil - - import torch - import torchvision.models as models - - RESNET50_LABEL_FILE = "resnet50_labels.txt" if FLAGS.openvino: import openvino.runtime as ov @@ -2701,24 +2008,19 @@ def create_fixed_models( # Make multiple versions of some models for version testing # (they use different version policies when created above) - # BF16 generation: TensorRT requires SM>=8.0; ONNX has no such - # requirement (model generation runs on CPU; ORT decides at runtime). - bf16_for_trt = FLAGS.tensorrt and tu.check_gpus_compute_capability( - min_capability=8.0 - ) - if FLAGS.tensorrt and not bf16_for_trt: - print( - "Skipping the generation of TensorRT PLAN models for the BF16 datatype!" - ) - if bf16_for_trt or FLAGS.onnx: - create_fixed_models( - FLAGS.models_dir, - np_dtype_bfloat16, - np_dtype_bfloat16, - np_dtype_bfloat16, - ) - if FLAGS.tensorrt: + if tu.check_gpus_compute_capability(min_capability=8.0): + create_fixed_models( + FLAGS.models_dir, + np_dtype_bfloat16, + np_dtype_bfloat16, + np_dtype_bfloat16, + ) + else: + print( + "Skipping the generation of TensorRT PLAN models for the BF16 datatype!" + ) + for vt in [np.float32, np.float16, np.int32, np.uint8]: create_plan_modelfile( FLAGS.models_dir, 8, 2, (16,), (16,), (16,), vt, vt, vt, swap=True @@ -2798,7 +2100,6 @@ def create_fixed_models( create_onnx_modelfile( FLAGS.models_dir, 0, 3, (16,), (16,), (16,), vt, vt, vt, swap=True ) - if FLAGS.libtorch: for vt in [np.float32, np.int32, np.int16, np.int8]: create_libtorch_modelfile( @@ -2813,7 +2114,6 @@ def create_fixed_models( create_libtorch_modelfile( FLAGS.models_dir, 0, 3, (16,), (16,), (16,), vt, vt, vt, swap=True ) - if FLAGS.openvino: for vt in [np.float16, np.float32, np.int8, np.int16, np.int32]: create_openvino_modelfile( @@ -2997,26 +2297,22 @@ def create_fixed_models( 32, ) - # BF16 generation: TensorRT requires SM>=8.0; ONNX has no such - # requirement (model generation runs on CPU; ORT decides at runtime). - bf16_for_trt = FLAGS.tensorrt and tu.check_gpus_compute_capability( - min_capability=8.0 - ) - if FLAGS.tensorrt and not bf16_for_trt: - print( - "Skipping the generation of TensorRT PLAN models for the BF16 datatype!" - ) - if bf16_for_trt or FLAGS.onnx: - create_models( - FLAGS.models_dir, - np_dtype_bfloat16, - np_dtype_bfloat16, - np_dtype_bfloat16, - (-1, -1), - (-1, -1), - (-1, -1), - 0, - ) + if FLAGS.tensorrt: + if tu.check_gpus_compute_capability(min_capability=8.0): + create_models( + FLAGS.models_dir, + np_dtype_bfloat16, + np_dtype_bfloat16, + np_dtype_bfloat16, + (-1, -1), + (-1, -1), + (-1, -1), + 0, + ) + else: + print( + "Skipping the generation of TensorRT PLAN models for the BF16 datatype!" + ) if FLAGS.ensemble: # Create utility models used in ensemble @@ -3033,16 +2329,3 @@ def create_fixed_models( # to fixed size model is not safe but doable for model_shape in [(-1,), (-1, -1), (-1, -1, -1)]: emu.create_nop_modelconfig(FLAGS.models_dir, model_shape, model_dtype) - - if FLAGS.torch_aoti: - print( - f"{_color_magenta}PyTorch: Complex AOTI model generation requested{_color_reset}" - ) - if create_torch_aoti_complex_model_file(FLAGS.models_dir): - create_torch_aoti_complex_model_config(FLAGS.models_dir) - - if FLAGS.torchvision_aoti: - # TODO: Add support for variable batch size and version policy for torchvision AOTI models. - print(f"{_color_blue}TorchVision AOTI model generation requested{_color_reset}") - if create_torchvision_aoti_model_file(FLAGS.models_dir, 1): - create_torchvision_aoti_model_config(FLAGS.models_dir, 1) diff --git a/qa/common/gen_qa_ort_scalar_models.py b/qa/common/gen_qa_ort_scalar_models.py index 116a688a39..c00a97d5ed 100755 --- a/qa/common/gen_qa_ort_scalar_models.py +++ b/qa/common/gen_qa_ort_scalar_models.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -68,7 +68,7 @@ def create_onnx_modelfile(models_dir, shape, dtype, model_version=1): try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir onnx.save(model_def, model_version_dir + "/model.onnx") @@ -103,7 +103,7 @@ def create_onnx_modelconfig(models_dir, dtype, shape): try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(config_dir + "/config.pbtxt", "w") as cfile: diff --git a/qa/common/gen_qa_ragged_models.py b/qa/common/gen_qa_ragged_models.py index a43888c398..de8c583d88 100755 --- a/qa/common/gen_qa_ragged_models.py +++ b/qa/common/gen_qa_ragged_models.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2020-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -57,11 +57,7 @@ def create_plan_modelfile(models_dir, model_version, dtype): # - BATCH_MAX_ELEMENT_COUNT_AS_SHAPE # - BATCH_ITEM_SHAPE_FLATTEN - TRT_LOGGER = ( - trt.Logger(trt.Logger.INFO) - if os.environ.get("TRT_VERBOSE") != "1" - else trt.Logger(trt.Logger.VERBOSE) - ) + TRT_LOGGER = trt.Logger(trt.Logger.INFO) builder = trt.Builder(TRT_LOGGER) network = builder.create_network() trt_dtype = np_to_trt_dtype(dtype) @@ -129,7 +125,7 @@ def create_plan_modelfile(models_dir, model_version, dtype): try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(model_version_dir + "/model.plan", "wb") as f: @@ -261,7 +257,7 @@ def create_onnx_modelfile(models_dir, model_version, dtype): try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir onnx.save(model_def, model_version_dir + "/model.onnx") @@ -324,13 +320,13 @@ def forward(self, BATCH_INPUT, BATCH_AND_SIZE_INPUT, RAGGED_INPUT): try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir traced.save(model_version_dir + "/model.pt") -def create_modelconfig(models_dir, max_batch, dtype, backend, platform): +def create_modelconfig(models_dir, max_batch, model_version, dtype, backend, platform): version_policy_str = "{ latest { num_versions: 1 }}" backend_spec = """ @@ -402,7 +398,7 @@ def create_modelconfig(models_dir, max_batch, dtype, backend, platform): try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(config_dir + "/config.pbtxt", "w") as cfile: @@ -416,11 +412,7 @@ def create_plan_itemshape_modelfile(models_dir, model_version, dtype): # generated to have matching batch dimension, the output can be produced # via identity op and expect Triton will scatter the output properly. - TRT_LOGGER = ( - trt.Logger(trt.Logger.INFO) - if os.environ.get("TRT_VERBOSE") != "1" - else trt.Logger(trt.Logger.VERBOSE) - ) + TRT_LOGGER = trt.Logger(trt.Logger.INFO) builder = trt.Builder(TRT_LOGGER) network = builder.create_network() trt_dtype = np_to_trt_dtype(dtype) @@ -455,7 +447,7 @@ def create_plan_itemshape_modelfile(models_dir, model_version, dtype): try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(model_version_dir + "/model.plan", "wb") as f: @@ -505,7 +497,7 @@ def create_onnx_itemshape_modelfile(models_dir, model_version, dtype): try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir onnx.save(model_def, model_version_dir + "/model.onnx") @@ -541,13 +533,15 @@ def forward(self, RAGGED_INPUT, BATCH_INPUT): try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir traced.save(model_version_dir + "/model.pt") -def create_itemshape_modelconfig(models_dir, max_batch, dtype, backend, platform): +def create_itemshape_modelconfig( + models_dir, max_batch, model_version, dtype, backend, platform +): version_policy_str = "{ latest { num_versions: 1 }}" backend_spec = """ @@ -599,7 +593,7 @@ def create_itemshape_modelconfig(models_dir, max_batch, dtype, backend, platform try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(config_dir + "/config.pbtxt", "w") as cfile: @@ -609,19 +603,29 @@ def create_itemshape_modelconfig(models_dir, max_batch, dtype, backend, platform def create_batch_input_models(models_dir): model_version = 1 if FLAGS.tensorrt: - create_modelconfig(models_dir, 4, np.float32, "tensorrt", "plan") + create_modelconfig(models_dir, 4, model_version, np.float32, "tensorrt", "plan") create_plan_modelfile(models_dir, model_version, np.float32) - create_itemshape_modelconfig(models_dir, 4, np.float32, "tensorrt", "plan") + create_itemshape_modelconfig( + models_dir, 4, model_version, np.float32, "tensorrt", "plan" + ) create_plan_itemshape_modelfile(models_dir, model_version, np.float32) if FLAGS.onnx: - create_modelconfig(models_dir, 4, np.float32, "onnxruntime", "onnx") + create_modelconfig( + models_dir, 4, model_version, np.float32, "onnxruntime", "onnx" + ) create_onnx_modelfile(models_dir, model_version, np.float32) - create_itemshape_modelconfig(models_dir, 4, np.float32, "onnxruntime", "onnx") + create_itemshape_modelconfig( + models_dir, 4, model_version, np.float32, "onnxruntime", "onnx" + ) create_onnx_itemshape_modelfile(models_dir, model_version, np.float32) if FLAGS.libtorch: - create_modelconfig(models_dir, 4, np.float32, "pytorch", "libtorch") + create_modelconfig( + models_dir, 4, model_version, np.float32, "pytorch", "libtorch" + ) create_libtorch_modelfile(models_dir, model_version, np.float32) - create_itemshape_modelconfig(models_dir, 4, np.float32, "pytorch", "libtorch") + create_itemshape_modelconfig( + models_dir, 4, model_version, np.float32, "pytorch", "libtorch" + ) create_libtorch_itemshape_modelfile(models_dir, model_version, np.float32) diff --git a/qa/common/gen_qa_reshape_models.py b/qa/common/gen_qa_reshape_models.py index 56d75f70ab..8193b29677 100755 --- a/qa/common/gen_qa_reshape_models.py +++ b/qa/common/gen_qa_reshape_models.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -49,18 +49,16 @@ def create_plan_modelfile( models_dir, model_version, max_batch, dtype, input_shapes, output_shapes ): assert len(input_shapes) == len(output_shapes) - if not tu.validate_for_trt_model(dtype, dtype, dtype): + if not tu.validate_for_trt_model( + dtype, dtype, dtype, input_shapes[0], input_shapes[0], input_shapes[0] + ): return trt_dtype = np_to_trt_dtype(dtype) io_cnt = len(input_shapes) # Create the model that copies inputs to corresponding outputs. - TRT_LOGGER = ( - trt.Logger(trt.Logger.INFO) - if os.environ.get("TRT_VERBOSE") != "1" - else trt.Logger(trt.Logger.VERBOSE) - ) + TRT_LOGGER = trt.Logger(trt.Logger.INFO) builder = trt.Builder(TRT_LOGGER) network = builder.create_network() @@ -116,7 +114,7 @@ def create_plan_modelfile( try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(model_version_dir + "/model.plan", "wb") as f: @@ -125,6 +123,7 @@ def create_plan_modelfile( def create_plan_modelconfig( models_dir, + model_version, max_batch, dtype, input_shapes, @@ -135,7 +134,9 @@ def create_plan_modelconfig( assert len(input_shapes) == len(input_model_shapes) assert len(output_shapes) == len(output_model_shapes) assert len(input_shapes) == len(output_shapes) - if not tu.validate_for_trt_model(dtype, dtype, dtype): + if not tu.validate_for_trt_model( + dtype, dtype, dtype, input_shapes[0], input_shapes[0], input_shapes[0] + ): return io_cnt = len(input_shapes) @@ -195,7 +196,7 @@ def create_plan_modelconfig( try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(config_dir + "/config.pbtxt", "w") as cfile: @@ -413,7 +414,7 @@ def forward(self, input0, input1, input2, input3): try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir traced.save(model_version_dir + "/model.pt") @@ -421,6 +422,7 @@ def forward(self, input0, input1, input2, input3): def create_libtorch_modelconfig( models_dir, + model_version, max_batch, dtype, input_shapes, @@ -500,7 +502,7 @@ def create_libtorch_modelconfig( try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(config_dir + "/config.pbtxt", "w") as cfile: @@ -511,7 +513,15 @@ def create_ensemble_modelfile( models_dir, model_version, max_batch, dtype, input_shapes, output_shapes ): assert len(input_shapes) == len(output_shapes) - if not tu.validate_for_ensemble_model("reshape", dtype, dtype, dtype): + if not tu.validate_for_ensemble_model( + "reshape", + dtype, + dtype, + dtype, + input_shapes[0], + input_shapes[0], + input_shapes[0], + ): return emu.create_identity_ensemble_modelfile( @@ -538,7 +548,15 @@ def create_ensemble_modelconfig( assert len(input_shapes) == len(input_model_shapes) assert len(output_shapes) == len(output_model_shapes) assert len(input_shapes) == len(output_shapes) - if not tu.validate_for_ensemble_model("reshape", dtype, dtype, dtype): + if not tu.validate_for_ensemble_model( + "reshape", + dtype, + dtype, + dtype, + input_shapes[0], + input_shapes[0], + input_shapes[0], + ): return # No reason to reshape ensemble inputs / outputs to empty as the inner models @@ -572,7 +590,9 @@ def create_onnx_modelfile( models_dir, model_version, max_batch, dtype, input_shapes, output_shapes ): assert len(input_shapes) == len(output_shapes) - if not tu.validate_for_onnx_model(dtype, dtype, dtype): + if not tu.validate_for_onnx_model( + dtype, dtype, dtype, input_shapes[0], input_shapes[0], input_shapes[0] + ): return onnx_dtype = np_to_onnx_dtype(dtype) @@ -632,7 +652,7 @@ def create_onnx_modelfile( try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir onnx.save(model_def, model_version_dir + "/model.onnx") @@ -640,6 +660,7 @@ def create_onnx_modelfile( def create_onnx_modelconfig( models_dir, + model_version, max_batch, dtype, input_shapes, @@ -650,7 +671,9 @@ def create_onnx_modelconfig( assert len(input_shapes) == len(input_model_shapes) assert len(output_shapes) == len(output_model_shapes) assert len(input_shapes) == len(output_shapes) - if not tu.validate_for_onnx_model(dtype, dtype, dtype): + if not tu.validate_for_onnx_model( + dtype, dtype, dtype, input_shapes[0], input_shapes[0], input_shapes[0] + ): return io_cnt = len(input_shapes) @@ -677,7 +700,7 @@ def create_onnx_modelconfig( try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(config_dir + "/config.pbtxt", "w") as cfile: @@ -700,6 +723,8 @@ def create_openvino_modelfile( dtype, dtype, batch_dim + input_shapes[0], + batch_dim + input_shapes[0], + batch_dim + input_shapes[0], ): return @@ -737,6 +762,7 @@ def create_openvino_modelfile( def create_openvino_modelconfig( models_dir, + model_version, max_batch, dtype, input_shapes, @@ -759,6 +785,8 @@ def create_openvino_modelconfig( dtype, dtype, batch_dim + input_shapes[0], + batch_dim + input_shapes[0], + batch_dim + input_shapes[0], ): return @@ -821,7 +849,7 @@ def create_openvino_modelconfig( try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(config_dir + "/config.pbtxt", "w") as cfile: @@ -846,6 +874,7 @@ def create_models( if FLAGS.onnx: create_onnx_modelconfig( models_dir, + model_version, 8, dtype, input_shapes, @@ -859,6 +888,7 @@ def create_models( if no_batch: create_onnx_modelconfig( models_dir, + model_version, 0, dtype, input_shapes, @@ -938,6 +968,7 @@ def create_trt_models( if FLAGS.tensorrt: create_plan_modelconfig( models_dir, + model_version, 8, dtype, input_shapes, @@ -951,6 +982,7 @@ def create_trt_models( if no_batch: create_plan_modelconfig( models_dir, + model_version, 0, dtype, input_shapes, @@ -986,6 +1018,7 @@ def create_libtorch_models( if FLAGS.libtorch: create_libtorch_modelconfig( models_dir, + model_version, 8, dtype, input_shapes, @@ -1000,6 +1033,7 @@ def create_libtorch_models( if no_batch and (dtype != np_dtype_string): create_libtorch_modelconfig( models_dir, + model_version, 0, dtype, input_shapes, @@ -1035,6 +1069,7 @@ def create_openvino_models( if FLAGS.openvino: create_openvino_modelconfig( models_dir, + model_version, 8, dtype, input_shapes, @@ -1048,6 +1083,7 @@ def create_openvino_models( if no_batch: create_openvino_modelconfig( models_dir, + model_version, 0, dtype, input_shapes, diff --git a/qa/common/gen_qa_sequence_models.py b/qa/common/gen_qa_sequence_models.py index f8d89a5f9e..ad31bbc0ba 100755 --- a/qa/common/gen_qa_sequence_models.py +++ b/qa/common/gen_qa_sequence_models.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -59,11 +59,7 @@ def create_plan_shape_tensor_modelfile( trt_shape_dtype = np_to_trt_dtype(shape_tensor_input_dtype) trt_memory_format = trt.TensorFormat.LINEAR - TRT_LOGGER = ( - trt.Logger(trt.Logger.INFO) - if os.environ.get("TRT_VERBOSE") != "1" - else trt.Logger(trt.Logger.VERBOSE) - ) + TRT_LOGGER = trt.Logger(trt.Logger.INFO) builder = trt.Builder(TRT_LOGGER) network = builder.create_network() @@ -118,8 +114,7 @@ def create_plan_shape_tensor_modelfile( flags = 1 << int(trt.BuilderFlag.DIRECT_IO) flags |= 1 << int(trt.BuilderFlag.PREFER_PRECISION_CONSTRAINTS) - if hasattr(trt.BuilderFlag, "REJECT_EMPTY_ALGORITHMS"): - flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) + flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) if trt_dtype == trt.int8: flags |= 1 << int(trt.BuilderFlag.INT8) @@ -175,7 +170,7 @@ def create_plan_shape_tensor_modelfile( try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(model_version_dir + "/model.plan", "wb") as f: @@ -187,11 +182,7 @@ def create_plan_modelfile(models_dir, model_version, max_batch, dtype, shape): # Create the model. For now don't implement a proper accumulator # just return 0 if not-ready and 'INPUT'+'START' otherwise... the # tests know to expect this. - TRT_LOGGER = ( - trt.Logger(trt.Logger.INFO) - if os.environ.get("TRT_VERBOSE") != "1" - else trt.Logger(trt.Logger.VERBOSE) - ) + TRT_LOGGER = trt.Logger(trt.Logger.INFO) builder = trt.Builder(TRT_LOGGER) network = builder.create_network() @@ -266,7 +257,7 @@ def create_plan_modelfile(models_dir, model_version, max_batch, dtype, shape): try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(model_version_dir + "/model.plan", "wb") as f: @@ -280,11 +271,7 @@ def create_plan_rf_modelfile(models_dir, model_version, max_batch, dtype, shape) # Create the model. For now don't implement a proper accumulator # just return 0 if not-ready and 'INPUT'+'START' otherwise... the # tests know to expect this. - TRT_LOGGER = ( - trt.Logger(trt.Logger.INFO) - if os.environ.get("TRT_VERBOSE") != "1" - else trt.Logger(trt.Logger.VERBOSE) - ) + TRT_LOGGER = trt.Logger(trt.Logger.INFO) builder = trt.Builder(TRT_LOGGER) network = builder.create_network() @@ -321,8 +308,7 @@ def create_plan_rf_modelfile(models_dir, model_version, max_batch, dtype, shape) flags = 1 << int(trt.BuilderFlag.DIRECT_IO) flags |= 1 << int(trt.BuilderFlag.PREFER_PRECISION_CONSTRAINTS) - if hasattr(trt.BuilderFlag, "REJECT_EMPTY_ALGORITHMS"): - flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) + flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) if trt_dtype == trt.int8: flags |= 1 << int(trt.BuilderFlag.INT8) @@ -384,7 +370,7 @@ def create_plan_rf_modelfile(models_dir, model_version, max_batch, dtype, shape) try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(model_version_dir + "/model.plan", "wb") as f: @@ -392,7 +378,7 @@ def create_plan_rf_modelfile(models_dir, model_version, max_batch, dtype, shape) def create_plan_models(models_dir, model_version, max_batch, dtype, shape): - if not tu.validate_for_trt_model(dtype, dtype, dtype): + if not tu.validate_for_trt_model(dtype, dtype, dtype, shape, shape, shape): return if dtype != np.float32: @@ -402,9 +388,9 @@ def create_plan_models(models_dir, model_version, max_batch, dtype, shape): def create_plan_modelconfig( - models_dir, max_batch, dtype, shape, shape_tensor_input_dtype=None + models_dir, model_version, max_batch, dtype, shape, shape_tensor_input_dtype=None ): - if not tu.validate_for_trt_model(dtype, dtype, dtype): + if not tu.validate_for_trt_model(dtype, dtype, dtype, shape, shape, shape): return model_name = tu.get_sequence_model_name( @@ -561,7 +547,7 @@ def create_plan_modelconfig( try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(config_dir + "/config.pbtxt", "w") as cfile: @@ -569,7 +555,7 @@ def create_plan_modelconfig( def create_onnx_modelfile(models_dir, model_version, max_batch, dtype, shape): - if not tu.validate_for_onnx_model(dtype, dtype, dtype): + if not tu.validate_for_onnx_model(dtype, dtype, dtype, shape, shape, shape): return model_name = tu.get_sequence_model_name( @@ -667,14 +653,14 @@ def create_onnx_modelfile(models_dir, model_version, max_batch, dtype, shape): try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir onnx.save(model_def, model_version_dir + "/model.onnx") -def create_onnx_modelconfig(models_dir, max_batch, dtype, shape): - if not tu.validate_for_onnx_model(dtype, dtype, dtype): +def create_onnx_modelconfig(models_dir, model_version, max_batch, dtype, shape): + if not tu.validate_for_onnx_model(dtype, dtype, dtype, shape, shape, shape): return model_name = tu.get_sequence_model_name( @@ -745,7 +731,7 @@ def create_onnx_modelconfig(models_dir, max_batch, dtype, shape): try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(config_dir + "/config.pbtxt", "w") as cfile: @@ -798,13 +784,13 @@ def forward(self, input0, start0, ready0): try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir traced.save(model_version_dir + "/model.pt") -def create_libtorch_modelconfig(models_dir, max_batch, dtype, shape): +def create_libtorch_modelconfig(models_dir, model_version, max_batch, dtype, shape): if not tu.validate_for_libtorch_model( dtype, dtype, dtype, shape, shape, shape, max_batch ): @@ -882,7 +868,7 @@ def create_libtorch_modelconfig(models_dir, max_batch, dtype, shape): try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(config_dir + "/config.pbtxt", "w") as cfile: @@ -897,7 +883,9 @@ def create_openvino_modelfile(models_dir, model_version, max_batch, dtype, shape max_batch, ] ) - if not tu.validate_for_openvino_model(dtype, dtype, dtype, batch_dim + shape): + if not tu.validate_for_openvino_model( + dtype, dtype, dtype, batch_dim + shape, batch_dim + shape, batch_dim + shape + ): return model_name = tu.get_sequence_model_name( @@ -916,7 +904,7 @@ def create_openvino_modelfile(models_dir, model_version, max_batch, dtype, shape openvino_save_model(model_version_dir, model) -def create_openvino_modelconfig(models_dir, max_batch, dtype, shape): +def create_openvino_modelconfig(models_dir, model_version, max_batch, dtype, shape): batch_dim = ( [] if max_batch == 0 @@ -924,7 +912,9 @@ def create_openvino_modelconfig(models_dir, max_batch, dtype, shape): max_batch, ] ) - if not tu.validate_for_openvino_model(dtype, dtype, dtype, batch_dim + shape): + if not tu.validate_for_openvino_model( + dtype, dtype, dtype, batch_dim + shape, batch_dim + shape, batch_dim + shape + ): return model_name = tu.get_sequence_model_name( @@ -984,7 +974,7 @@ def create_openvino_modelconfig(models_dir, max_batch, dtype, shape): try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(config_dir + "/config.pbtxt", "w") as cfile: @@ -996,12 +986,16 @@ def create_shape_tensor_models( ): model_version = 1 - create_plan_modelconfig(models_dir, 8, dtype, shape, shape_tensor_input_dtype) + create_plan_modelconfig( + models_dir, model_version, 8, dtype, shape, shape_tensor_input_dtype + ) create_plan_shape_tensor_modelfile( models_dir, model_version, 8, dtype, shape, shape_tensor_input_dtype ) if no_batch: - create_plan_modelconfig(models_dir, 0, dtype, shape, shape_tensor_input_dtype) + create_plan_modelconfig( + models_dir, model_version, 0, dtype, shape, shape_tensor_input_dtype + ) create_plan_shape_tensor_modelfile( models_dir, model_version, 0, dtype, shape, shape_tensor_input_dtype ) @@ -1017,32 +1011,32 @@ def create_models(models_dir, dtype, shape, no_batch=True): if dtype == np.int8: suffix = [1, 1] - create_plan_modelconfig(models_dir, 8, dtype, shape + suffix) + create_plan_modelconfig(models_dir, model_version, 8, dtype, shape + suffix) create_plan_models(models_dir, model_version, 8, dtype, shape + suffix) if no_batch: - create_plan_modelconfig(models_dir, 0, dtype, shape + suffix) + create_plan_modelconfig(models_dir, model_version, 0, dtype, shape + suffix) create_plan_models(models_dir, model_version, 0, dtype, shape + suffix) if FLAGS.onnx: - create_onnx_modelconfig(models_dir, 8, dtype, shape) + create_onnx_modelconfig(models_dir, model_version, 8, dtype, shape) create_onnx_modelfile(models_dir, model_version, 8, dtype, shape) if no_batch: - create_onnx_modelconfig(models_dir, 0, dtype, shape) + create_onnx_modelconfig(models_dir, model_version, 0, dtype, shape) create_onnx_modelfile(models_dir, model_version, 0, dtype, shape) # Skip for PyTorch String I/O if FLAGS.libtorch and (dtype != np_dtype_string): - create_libtorch_modelconfig(models_dir, 8, dtype, shape) + create_libtorch_modelconfig(models_dir, model_version, 8, dtype, shape) create_libtorch_modelfile(models_dir, model_version, 8, dtype, shape) if no_batch: - create_libtorch_modelconfig(models_dir, 0, dtype, shape) + create_libtorch_modelconfig(models_dir, model_version, 0, dtype, shape) create_libtorch_modelfile(models_dir, model_version, 0, dtype, shape) if FLAGS.openvino: - create_openvino_modelconfig(models_dir, 8, dtype, shape) + create_openvino_modelconfig(models_dir, model_version, 8, dtype, shape) create_openvino_modelfile(models_dir, model_version, 8, dtype, shape) if no_batch: - create_openvino_modelconfig(models_dir, 0, dtype, shape) + create_openvino_modelconfig(models_dir, model_version, 0, dtype, shape) create_openvino_modelfile(models_dir, model_version, 0, dtype, shape) if FLAGS.ensemble: diff --git a/qa/common/gen_qa_torchtrt_models.py b/qa/common/gen_qa_torchtrt_models.py index 3abfec1e2c..5f6ea04581 100755 --- a/qa/common/gen_qa_torchtrt_models.py +++ b/qa/common/gen_qa_torchtrt_models.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -28,21 +28,11 @@ import argparse import os -import sys import torch +import torch_tensorrt import torchvision -try: - import torch_tensorrt -except ImportError: - print( - "WARNING: torch_tensorrt is not available in this environment. " - "Skipping Torch-TensorRT model generation.", - file=sys.stderr, - ) - sys.exit(0) - def create_resnet50_torchtrt(models_dir, max_batch): model = torchvision.models.resnet50(pretrained=True) @@ -71,7 +61,7 @@ def create_resnet50_torchtrt(models_dir, max_batch): try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir torch.jit.save(trt_ts_module, model_version_dir + "/model.pt") @@ -106,7 +96,7 @@ def create_resnet50_torchtrt_modelconfig(models_dir, max_batch): try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(config_dir + "/config.pbtxt", "w") as cfile: diff --git a/qa/common/gen_qa_trt_data_dependent_shape.py b/qa/common/gen_qa_trt_data_dependent_shape.py index 8984059411..c6f4bf2b5e 100755 --- a/qa/common/gen_qa_trt_data_dependent_shape.py +++ b/qa/common/gen_qa_trt_data_dependent_shape.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -45,11 +45,7 @@ def create_data_dependent_modelfile( trt_input_dtype = np_to_trt_dtype(input_dtype) # Create the model - TRT_LOGGER = ( - trt.Logger(trt.Logger.INFO) - if os.environ.get("TRT_VERBOSE") != "1" - else trt.Logger(trt.Logger.VERBOSE) - ) + TRT_LOGGER = trt.Logger(trt.Logger.INFO) builder = trt.Builder(TRT_LOGGER) network = builder.create_network() @@ -86,17 +82,11 @@ def create_data_dependent_modelfile( # serialized model engine_bytes = builder.build_serialized_network(network, config) - if engine_bytes is None: - print( - f"warning: Skipping {model_name}: TRT engine build failed " - f"(NonZero op may not be supported on this GPU/TRT version)" - ) - return model_version_dir = models_dir + "/" + model_name + "/1" try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(model_version_dir + "/model.plan", "wb") as f: @@ -135,7 +125,7 @@ def create_data_dependent_modelconfig( try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(config_dir + "/config.pbtxt", "w") as cfile: diff --git a/qa/common/gen_qa_trt_format_models.py b/qa/common/gen_qa_trt_format_models.py index 5f2cadd69e..6419a6e2ab 100755 --- a/qa/common/gen_qa_trt_format_models.py +++ b/qa/common/gen_qa_trt_format_models.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2020-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2020-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -81,11 +81,7 @@ def create_plan_modelfile( trt_output_memory_format = output_memory_format # Create the model - TRT_LOGGER = ( - trt.Logger(trt.Logger.INFO) - if os.environ.get("TRT_VERBOSE") != "1" - else trt.Logger(trt.Logger.VERBOSE) - ) + TRT_LOGGER = trt.Logger(trt.Logger.INFO) builder = trt.Builder(TRT_LOGGER) network = builder.create_network() if max_batch == 0: @@ -106,8 +102,8 @@ def create_plan_modelfile( network.mark_output(out0.get_output(0)) network.mark_output(out1.get_output(0)) - out0.set_output_type(0, trt_output0_dtype) - out1.set_output_type(0, trt_output1_dtype) + out0.get_output(0).dtype = trt_output0_dtype + out1.get_output(0).dtype = trt_output1_dtype in0.allowed_formats = 1 << int(trt_input_memory_format) in1.allowed_formats = 1 << int(trt_input_memory_format) @@ -147,8 +143,7 @@ def create_plan_modelfile( # The build will fail if TensorRT cannot build an engine without introducing such reformatting. The failure may happen only for some target platforms, because of what formats are supported by kernels for those platforms. # flags = 1 << int(trt.BuilderFlag.DIRECT_IO) flags = 1 << int(trt.BuilderFlag.PREFER_PRECISION_CONSTRAINTS) - if hasattr(trt.BuilderFlag, "REJECT_EMPTY_ALGORITHMS"): - flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) + flags |= 1 << int(trt.BuilderFlag.REJECT_EMPTY_ALGORITHMS) datatype_set = set([trt_input_dtype, trt_output0_dtype, trt_output1_dtype]) for dt in datatype_set: if dt == trt.int8: @@ -180,7 +175,7 @@ def create_plan_modelfile( try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(model_version_dir + "/model.plan", "wb") as f: @@ -190,6 +185,7 @@ def create_plan_modelfile( def create_plan_modelconfig( models_dir, max_batch, + model_version, input_shape, output0_shape, output1_shape, @@ -204,6 +200,9 @@ def create_plan_modelconfig( input_dtype, output0_dtype, output1_dtype, + input_shape, + output0_shape, + output1_shape, ): return @@ -325,7 +324,7 @@ def create_plan_modelconfig( try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(config_dir + "/config.pbtxt", "w") as cfile: @@ -349,12 +348,16 @@ def create_plan_model( input_dtype, output0_dtype, output1_dtype, + input_shape, + output0_shape, + output1_shape, ): return create_plan_modelconfig( models_dir, max_batch, + model_version, input_shape, output0_shape, output1_shape, diff --git a/qa/common/gen_qa_trt_plugin_models.py b/qa/common/gen_qa_trt_plugin_models.py index 9fd23d92a8..0e2e9cf698 100755 --- a/qa/common/gen_qa_trt_plugin_models.py +++ b/qa/common/gen_qa_trt_plugin_models.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -44,12 +44,7 @@ def get_trt_plugin(plugin_name): plugin = None field_collection = None - # The upstream onnx_custom_plugin sample is V2 on TRT 10.x release - # branches and V3 on rel-11.0 (and TRT 11 removed the V2 plugin - # registry surface). Pick the matching API at runtime. - registry = trt.get_plugin_registry() - use_v3 = not hasattr(registry, "plugin_creator_list") - plugin_creators = registry.all_creators if use_v3 else registry.plugin_creator_list + plugin_creators = trt.get_plugin_registry().plugin_creator_list for plugin_creator in plugin_creators: if (plugin_creator.name == "CustomHardmax") and ( plugin_name == "CustomHardmax" @@ -62,16 +57,9 @@ def get_trt_plugin(plugin_name): if field_collection is None: raise RuntimeError("Plugin not found: " + plugin_name) - if use_v3: - plugin = plugin_creator.create_plugin( - name=plugin_name, - field_collection=field_collection, - phase=trt.TensorRTPhase.BUILD, - ) - else: - plugin = plugin_creator.create_plugin( - name=plugin_name, field_collection=field_collection - ) + plugin = plugin_creator.create_plugin( + name=plugin_name, field_collection=field_collection + ) return plugin @@ -90,6 +78,9 @@ def create_plan_modelfile( input_dtype, output0_dtype, output0_dtype, + input_shape, + output0_shape, + output0_shape, ): return @@ -116,16 +107,9 @@ def create_plan_modelfile( input_layer = network.add_input( name="INPUT0", dtype=trt_input_dtype, shape=input_with_batchsize ) - # add_plugin_v2 was removed in TRT 11; add_plugin_v3 has existed since - # TRT 10.0. Pick the API that exists on this TRT install; the plugin - # object returned by get_trt_plugin() is matched to the same version. - plugin_obj = get_trt_plugin(plugin_name) - if hasattr(network, "add_plugin_v2"): - plugin_layer = network.add_plugin_v2(inputs=[input_layer], plugin=plugin_obj) - else: - plugin_layer = network.add_plugin_v3( - inputs=[input_layer], shape_inputs=[], plugin=plugin_obj - ) + plugin_layer = network.add_plugin_v2( + inputs=[input_layer], plugin=get_trt_plugin(plugin_name) + ) plugin_layer.get_output(0).name = "OUTPUT0" network.mark_output(plugin_layer.get_output(0)) @@ -163,7 +147,7 @@ def create_plan_modelfile( try: os.makedirs(model_version_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(model_version_dir + "/model.plan", "wb") as f: @@ -173,6 +157,7 @@ def create_plan_modelfile( def create_plan_modelconfig( models_dir, max_batch, + model_version, plugin_name, input_shape, output0_shape, @@ -183,6 +168,9 @@ def create_plan_modelconfig( input_dtype, output0_dtype, output0_dtype, + input_shape, + output0_shape, + output0_shape, ): return @@ -231,7 +219,7 @@ def create_plan_modelconfig( try: os.makedirs(config_dir) - except OSError: + except OSError as ex: pass # ignore existing dir with open(config_dir + "/config.pbtxt", "w") as cfile: @@ -245,6 +233,7 @@ def create_plugin_models(models_dir): create_plan_modelconfig( models_dir, 8, + model_version, "CustomHardmax", (2, 2), (2, 2), @@ -265,6 +254,7 @@ def create_plugin_models(models_dir): create_plan_modelconfig( models_dir, 0, + model_version, "CustomHardmax", (16, 1, 1), (16, 1, 1), diff --git a/qa/common/infer_test.py b/qa/common/infer_test.py index caa2d4f31e..aa06197373 100755 --- a/qa/common/infer_test.py +++ b/qa/common/infer_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2020-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -94,6 +94,9 @@ def _infer_exact_helper( input_dtype, output0_dtype, output1_dtype, + (input_size, 1, 1), + (input_size, 1, 1), + (input_size, 1, 1), ): if "plan" in TEST_BACKENDS: if input_dtype == np.int8: @@ -117,6 +120,9 @@ def _infer_exact_helper( input_dtype, output0_dtype, output1_dtype, + (input_size,), + (input_size,), + (input_size,), ): if "onnx" in TEST_BACKENDS: _infer_exact_helper( diff --git a/qa/common/infer_util.py b/qa/common/infer_util.py index 7e9c775570..edaf0ede47 100755 --- a/qa/common/infer_util.py +++ b/qa/common/infer_util.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -1367,13 +1367,11 @@ def shm_basic_infer( big_shm_name="", big_shm_size=64, default_shm_byte_size=64, - register_offset=0, shm_output_offset=0, shm_output_byte_size=64, protocol="http", use_system_shared_memory=False, use_cuda_shared_memory=False, - override_model_name=None, ): # Lazy shm imports... if use_system_shared_memory: @@ -1383,16 +1381,6 @@ def shm_basic_infer( else: raise Exception("No shared memory type specified") - if override_model_name is None: - model_name = "simple" - else: - model_name = override_model_name - - if model_name.startswith("libtorch"): - output_names = ["OUTPUT__0", "OUTPUT__1"] - else: - output_names = ["OUTPUT0", "OUTPUT1"] - input0_data = np.arange(start=0, stop=16, dtype=np.int32) input1_data = np.ones(shape=16, dtype=np.int32) inputs = [] @@ -1400,17 +1388,13 @@ def shm_basic_infer( if protocol == "http": inputs.append(httpclient.InferInput("INPUT0", [1, 16], "INT32")) inputs.append(httpclient.InferInput("INPUT1", [1, 16], "INT32")) - outputs.append( - httpclient.InferRequestedOutput(output_names[0], binary_data=True) - ) - outputs.append( - httpclient.InferRequestedOutput(output_names[1], binary_data=False) - ) + outputs.append(httpclient.InferRequestedOutput("OUTPUT0", binary_data=True)) + outputs.append(httpclient.InferRequestedOutput("OUTPUT1", binary_data=False)) else: inputs.append(grpcclient.InferInput("INPUT0", [1, 16], "INT32")) inputs.append(grpcclient.InferInput("INPUT1", [1, 16], "INT32")) - outputs.append(grpcclient.InferRequestedOutput(output_names[0])) - outputs.append(grpcclient.InferRequestedOutput(output_names[1])) + outputs.append(grpcclient.InferRequestedOutput("OUTPUT0")) + outputs.append(grpcclient.InferRequestedOutput("OUTPUT1")) inputs[0].set_shared_memory("input0_data", default_shm_byte_size) @@ -1430,9 +1414,9 @@ def shm_basic_infer( try: results = triton_client.infer( - model_name, inputs, model_version="", outputs=outputs + "simple", inputs, model_version="", outputs=outputs ) - output = results.get_output(output_names[0]) + output = results.get_output("OUTPUT0") if protocol == "http": output_datatype = output["datatype"] output_shape = output["shape"] @@ -1443,16 +1427,11 @@ def shm_basic_infer( if use_system_shared_memory: output_data = shm.get_contents_as_numpy( - shm_op0_handle, - output_dtype, - output_shape, - offset=register_offset + shm_output_offset, + shm_op0_handle, output_dtype, output_shape ) elif use_cuda_shared_memory: output_data = cudashm.get_contents_as_numpy( - shm_op0_handle, - output_dtype, - output_shape, + shm_op0_handle, output_dtype, output_shape ) tester.assertTrue( diff --git a/qa/common/resnet50_labels.txt b/qa/common/resnet50_labels.txt deleted file mode 100644 index f40829ed0f..0000000000 --- a/qa/common/resnet50_labels.txt +++ /dev/null @@ -1,1000 +0,0 @@ -tench -goldfish -great white shark -tiger shark -hammerhead -electric ray -stingray -cock -hen -ostrich -brambling -goldfinch -house finch -junco -indigo bunting -robin -bulbul -jay -magpie -chickadee -water ouzel -kite -bald eagle -vulture -great grey owl -European fire salamander -common newt -eft -spotted salamander -axolotl -bullfrog -tree frog -tailed frog -loggerhead -leatherback turtle -mud turtle -terrapin -box turtle -banded gecko -common iguana -American chameleon -whiptail -agama -frilled lizard -alligator lizard -Gila monster -green lizard -African chameleon -Komodo dragon -African crocodile -American alligator -triceratops -thunder snake -ringneck snake -hognose snake -green snake -king snake -garter snake -water snake -vine snake -night snake -boa constrictor -rock python -Indian cobra -green mamba -sea snake -horned viper -diamondback -sidewinder -trilobite -harvestman -scorpion -black and gold garden spider -barn spider -garden spider -black widow -tarantula -wolf spider -tick -centipede -black grouse -ptarmigan -ruffed grouse -prairie chicken -peacock -quail -partridge -African grey -macaw -sulphur-crested cockatoo -lorikeet -coucal -bee eater -hornbill -hummingbird -jacamar -toucan -drake -red-breasted merganser -goose -black swan -tusker -echidna -platypus -wallaby -koala -wombat -jellyfish -sea anemone -brain coral -flatworm -nematode -conch -snail -slug -sea slug -chiton -chambered nautilus -Dungeness crab -rock crab -fiddler crab -king crab -American lobster -spiny lobster -crayfish -hermit crab -isopod -white stork -black stork -spoonbill -flamingo -little blue heron -American egret -bittern -crane -limpkin -European gallinule -American coot -bustard -ruddy turnstone -red-backed sandpiper -redshank -dowitcher -oystercatcher -pelican -king penguin -albatross -grey whale -killer whale -dugong -sea lion -Chihuahua -Japanese spaniel -Maltese dog -Pekinese -Shih-Tzu -Blenheim spaniel -papillon -toy terrier -Rhodesian ridgeback -Afghan hound -basset -beagle -bloodhound -bluetick -black-and-tan coonhound -Walker hound -English foxhound -redbone -borzoi -Irish wolfhound -Italian greyhound -whippet -Ibizan hound -Norwegian elkhound -otterhound -Saluki -Scottish deerhound -Weimaraner -Staffordshire bullterrier -American Staffordshire terrier -Bedlington terrier -Border terrier -Kerry blue terrier -Irish terrier -Norfolk terrier -Norwich terrier -Yorkshire terrier -wire-haired fox terrier -Lakeland terrier -Sealyham terrier -Airedale -cairn -Australian terrier -Dandie Dinmont -Boston bull -miniature schnauzer -giant schnauzer -standard schnauzer -Scotch terrier -Tibetan terrier -silky terrier -soft-coated wheaten terrier -West Highland white terrier -Lhasa -flat-coated retriever -curly-coated retriever -golden retriever -Labrador retriever -Chesapeake Bay retriever -German short-haired pointer -vizsla -English setter -Irish setter -Gordon setter -Brittany spaniel -clumber -English springer -Welsh springer spaniel -cocker spaniel -Sussex spaniel -Irish water spaniel -kuvasz -schipperke -groenendael -malinois -briard -kelpie -komondor -Old English sheepdog -Shetland sheepdog -collie -Border collie -Bouvier des Flandres -Rottweiler -German shepherd -Doberman -miniature pinscher -Greater Swiss Mountain dog -Bernese mountain dog -Appenzeller -EntleBucher -boxer -bull mastiff -Tibetan mastiff -French bulldog -Great Dane -Saint Bernard -Eskimo dog -malamute -Siberian husky -dalmatian -affenpinscher -basenji -pug -Leonberg -Newfoundland -Great Pyrenees -Samoyed -Pomeranian -chow -keeshond -Brabancon griffon -Pembroke -Cardigan -toy poodle -miniature poodle -standard poodle -Mexican hairless -timber wolf -white wolf -red wolf -coyote -dingo -dhole -African hunting dog -hyena -red fox -kit fox -Arctic fox -grey fox -tabby -tiger cat -Persian cat -Siamese cat -Egyptian cat -cougar -lynx -leopard -snow leopard -jaguar -lion -tiger -cheetah -brown bear -American black bear -ice bear -sloth bear -mongoose -meerkat -tiger beetle -ladybug -ground beetle -long-horned beetle -leaf beetle -dung beetle -rhinoceros beetle -weevil -fly -bee -ant -grasshopper -cricket -walking stick -cockroach -mantis -cicada -leafhopper -lacewing -dragonfly -damselfly -admiral -ringlet -monarch -cabbage butterfly -sulphur butterfly -lycaenid -starfish -sea urchin -sea cucumber -wood rabbit -hare -Angora -hamster -porcupine -fox squirrel -marmot -beaver -guinea pig -sorrel -zebra -hog -wild boar -warthog -hippopotamus -ox -water buffalo -bison -ram -bighorn -ibex -hartebeest -impala -gazelle -Arabian camel -llama -weasel -mink -polecat -black-footed ferret -otter -skunk -badger -armadillo -three-toed sloth -orangutan -gorilla -chimpanzee -gibbon -siamang -guenon -patas -baboon -macaque -langur -colobus -proboscis monkey -marmoset -capuchin -howler monkey -titi -spider monkey -squirrel monkey -Madagascar cat -indri -Indian elephant -African elephant -lesser panda -giant panda -barracouta -eel -coho -rock beauty -anemone fish -sturgeon -gar -lionfish -puffer -abacus -abaya -academic gown -accordion -acoustic guitar -aircraft carrier -airliner -airship -altar -ambulance -amphibian -analog clock -apiary -apron -ashcan -assault rifle -backpack -bakery -balance beam -balloon -ballpoint -Band Aid -banjo -bannister -barbell -barber chair -barbershop -barn -barometer -barrel -barrow -baseball -basketball -bassinet -bassoon -bathing cap -bath towel -bathtub -beach wagon -beacon -beaker -bearskin -beer bottle -beer glass -bell cote -bib -bicycle-built-for-two -bikini -binder -binoculars -birdhouse -boathouse -bobsled -bolo tie -bonnet -bookcase -bookshop -bottlecap -bow -bow tie -brass -brassiere -breakwater -breastplate -broom -bucket -buckle -bulletproof vest -bullet train -butcher shop -cab -caldron -candle -cannon -canoe -can opener -cardigan -car mirror -carousel -carpenter's kit -carton -car wheel -cash machine -cassette -cassette player -castle -catamaran -CD player -cello -cellular telephone -chain -chainlink fence -chain mail -chain saw -chest -chiffonier -chime -china cabinet -Christmas stocking -church -cinema -cleaver -cliff dwelling -cloak -clog -cocktail shaker -coffee mug -coffeepot -coil -combination lock -computer keyboard -confectionery -container ship -convertible -corkscrew -cornet -cowboy boot -cowboy hat -cradle -crane -crash helmet -crate -crib -Crock Pot -croquet ball -crutch -cuirass -dam -desk -desktop computer -dial telephone -diaper -digital clock -digital watch -dining table -dishrag -dishwasher -disk brake -dock -dogsled -dome -doormat -drilling platform -drum -drumstick -dumbbell -Dutch oven -electric fan -electric guitar -electric locomotive -entertainment center -envelope -espresso maker -face powder -feather boa -file -fireboat -fire engine -fire screen -flagpole -flute -folding chair -football helmet -forklift -fountain -fountain pen -four-poster -freight car -French horn -frying pan -fur coat -garbage truck -gasmask -gas pump -goblet -go-kart -golf ball -golfcart -gondola -gong -gown -grand piano -greenhouse -grille -grocery store -guillotine -hair slide -hair spray -half track -hammer -hamper -hand blower -hand-held computer -handkerchief -hard disc -harmonica -harp -harvester -hatchet -holster -home theater -honeycomb -hook -hoopskirt -horizontal bar -horse cart -hourglass -iPod -iron -jack-o'-lantern -jean -jeep -jersey -jigsaw puzzle -jinrikisha -joystick -kimono -knee pad -knot -lab coat -ladle -lampshade -laptop -lawn mower -lens cap -letter opener -library -lifeboat -lighter -limousine -liner -lipstick -Loafer -lotion -loudspeaker -loupe -lumbermill -magnetic compass -mailbag -mailbox -maillot -maillot -manhole cover -maraca -marimba -mask -matchstick -maypole -maze -measuring cup -medicine chest -megalith -microphone -microwave -military uniform -milk can -minibus -miniskirt -minivan -missile -mitten -mixing bowl -mobile home -Model T -modem -monastery -monitor -moped -mortar -mortarboard -mosque -mosquito net -motor scooter -mountain bike -mountain tent -mouse -mousetrap -moving van -muzzle -nail -neck brace -necklace -nipple -notebook -obelisk -oboe -ocarina -odometer -oil filter -organ -oscilloscope -overskirt -oxcart -oxygen mask -packet -paddle -paddlewheel -padlock -paintbrush -pajama -palace -panpipe -paper towel -parachute -parallel bars -park bench -parking meter -passenger car -patio -pay-phone -pedestal -pencil box -pencil sharpener -perfume -Petri dish -photocopier -pick -pickelhaube -picket fence -pickup -pier -piggy bank -pill bottle -pillow -ping-pong ball -pinwheel -pirate -pitcher -plane -planetarium -plastic bag -plate rack -plow -plunger -Polaroid camera -pole -police van -poncho -pool table -pop bottle -pot -potter's wheel -power drill -prayer rug -printer -prison -projectile -projector -puck -punching bag -purse -quill -quilt -racer -racket -radiator -radio -radio telescope -rain barrel -recreational vehicle -reel -reflex camera -refrigerator -remote control -restaurant -revolver -rifle -rocking chair -rotisserie -rubber eraser -rugby ball -rule -running shoe -safe -safety pin -saltshaker -sandal -sarong -sax -scabbard -scale -school bus -schooner -scoreboard -screen -screw -screwdriver -seat belt -sewing machine -shield -shoe shop -shoji -shopping basket -shopping cart -shovel -shower cap -shower curtain -ski -ski mask -sleeping bag -slide rule -sliding door -slot -snorkel -snowmobile -snowplow -soap dispenser -soccer ball -sock -solar dish -sombrero -soup bowl -space bar -space heater -space shuttle -spatula -speedboat -spider web -spindle -sports car -spotlight -stage -steam locomotive -steel arch bridge -steel drum -stethoscope -stole -stone wall -stopwatch -stove -strainer -streetcar -stretcher -studio couch -stupa -submarine -suit -sundial -sunglass -sunglasses -sunscreen -suspension bridge -swab -sweatshirt -swimming trunks -swing -switch -syringe -table lamp -tank -tape player -teapot -teddy -television -tennis ball -thatch -theater curtain -thimble -thresher -throne -tile roof -toaster -tobacco shop -toilet seat -torch -totem pole -tow truck -toyshop -tractor -trailer truck -tray -trench coat -tricycle -trimaran -tripod -triumphal arch -trolleybus -trombone -tub -turnstile -typewriter keyboard -umbrella -unicycle -upright -vacuum -vase -vault -velvet -vending machine -vestment -viaduct -violin -volleyball -waffle iron -wall clock -wallet -wardrobe -warplane -washbasin -washer -water bottle -water jug -water tower -whiskey jug -whistle -wig -window screen -window shade -Windsor tie -wine bottle -wing -wok -wooden spoon -wool -worm fence -wreck -yawl -yurt -web site -comic book -crossword puzzle -street sign -traffic light -book jacket -menu -plate -guacamole -consomme -hot pot -trifle -ice cream -ice lolly -French loaf -bagel -pretzel -cheeseburger -hotdog -mashed potato -head cabbage -broccoli -cauliflower -zucchini -spaghetti squash -acorn squash -butternut squash -cucumber -artichoke -bell pepper -cardoon -mushroom -Granny Smith -strawberry -orange -lemon -fig -pineapple -banana -jackfruit -custard apple -pomegranate -hay -carbonara -chocolate sauce -dough -meat loaf -pizza -potpie -burrito -red wine -espresso -cup -eggnog -alp -bubble -cliff -coral reef -geyser -lakeside -promontory -sandbar -seashore -valley -volcano -ballplayer -groom -scuba diver -rapeseed -daisy -yellow lady's slipper -corn -acorn -hip -buckeye -coral fungus -agaric -gyromitra -stinkhorn -earthstar -hen-of-the-woods -bolete -ear -toilet tissue diff --git a/qa/common/shm_util.py b/qa/common/shm_util.py index 6f66b422b7..0e533bcdbb 100755 --- a/qa/common/shm_util.py +++ b/qa/common/shm_util.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -27,7 +27,6 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import os -import threading import time from ctypes import * from os import listdir @@ -36,8 +35,6 @@ import tritonclient.http as httpclient from tritonclient.utils import * -CREATION_LOCK = threading.Lock() - # By default, find tritonserver on "localhost", but can be overridden # with TRITONSERVER_IPADDR envvar _tritonserver_ipaddr = os.environ.get("TRITONSERVER_IPADDR", "localhost") @@ -97,69 +94,64 @@ def create_set_shm_regions( shm_op0_handle = None shm_op1_handle = None - with CREATION_LOCK: - if use_system_shared_memory: - shm_ip0_handle = shm.create_shared_memory_region( - shm_region_names[0] + "_data", - "/" + shm_region_names[0], - input0_byte_size, - ) - shm_ip1_handle = shm.create_shared_memory_region( - shm_region_names[1] + "_data", - "/" + shm_region_names[1], - input1_byte_size, - ) + if use_system_shared_memory: + shm_ip0_handle = shm.create_shared_memory_region( + shm_region_names[0] + "_data", "/" + shm_region_names[0], input0_byte_size + ) + shm_ip1_handle = shm.create_shared_memory_region( + shm_region_names[1] + "_data", "/" + shm_region_names[1], input1_byte_size + ) - i = 0 - if "OUTPUT0" in outputs: - if precreated_shm_regions is None: - shm_op0_handle = shm.create_shared_memory_region( - shm_region_names[2] + "_data", - "/" + shm_region_names[2], - output0_byte_size, - ) - else: - shm_op0_handle = precreated_shm_regions[0] - i += 1 - if "OUTPUT1" in outputs: - if precreated_shm_regions is None: - shm_op1_handle = shm.create_shared_memory_region( - shm_region_names[2 + i] + "_data", - "/" + shm_region_names[2 + i], - output1_byte_size, - ) - else: - shm_op1_handle = precreated_shm_regions[i] + i = 0 + if "OUTPUT0" in outputs: + if precreated_shm_regions is None: + shm_op0_handle = shm.create_shared_memory_region( + shm_region_names[2] + "_data", + "/" + shm_region_names[2], + output0_byte_size, + ) + else: + shm_op0_handle = precreated_shm_regions[0] + i += 1 + if "OUTPUT1" in outputs: + if precreated_shm_regions is None: + shm_op1_handle = shm.create_shared_memory_region( + shm_region_names[2 + i] + "_data", + "/" + shm_region_names[2 + i], + output1_byte_size, + ) + else: + shm_op1_handle = precreated_shm_regions[i] - shm.set_shared_memory_region(shm_ip0_handle, input0_list) - shm.set_shared_memory_region(shm_ip1_handle, input1_list) + shm.set_shared_memory_region(shm_ip0_handle, input0_list) + shm.set_shared_memory_region(shm_ip1_handle, input1_list) - if use_cuda_shared_memory: - shm_ip0_handle = cudashm.create_shared_memory_region( - shm_region_names[0] + "_data", input0_byte_size, 0 - ) - shm_ip1_handle = cudashm.create_shared_memory_region( - shm_region_names[1] + "_data", input1_byte_size, 0 - ) - i = 0 - if "OUTPUT0" in outputs: - if precreated_shm_regions is None: - shm_op0_handle = cudashm.create_shared_memory_region( - shm_region_names[2] + "_data", output0_byte_size, 0 - ) - else: - shm_op0_handle = precreated_shm_regions[0] - i += 1 - if "OUTPUT1" in outputs: - if precreated_shm_regions is None: - shm_op1_handle = cudashm.create_shared_memory_region( - shm_region_names[2 + i] + "_data", output1_byte_size, 0 - ) - else: - shm_op1_handle = precreated_shm_regions[i] + if use_cuda_shared_memory: + shm_ip0_handle = cudashm.create_shared_memory_region( + shm_region_names[0] + "_data", input0_byte_size, 0 + ) + shm_ip1_handle = cudashm.create_shared_memory_region( + shm_region_names[1] + "_data", input1_byte_size, 0 + ) + i = 0 + if "OUTPUT0" in outputs: + if precreated_shm_regions is None: + shm_op0_handle = cudashm.create_shared_memory_region( + shm_region_names[2] + "_data", output0_byte_size, 0 + ) + else: + shm_op0_handle = precreated_shm_regions[0] + i += 1 + if "OUTPUT1" in outputs: + if precreated_shm_regions is None: + shm_op1_handle = cudashm.create_shared_memory_region( + shm_region_names[2 + i] + "_data", output1_byte_size, 0 + ) + else: + shm_op1_handle = precreated_shm_regions[i] - cudashm.set_shared_memory_region(shm_ip0_handle, input0_list) - cudashm.set_shared_memory_region(shm_ip1_handle, input1_list) + cudashm.set_shared_memory_region(shm_ip0_handle, input0_list) + cudashm.set_shared_memory_region(shm_ip1_handle, input1_list) return shm_region_names, [ shm_ip0_handle, @@ -345,27 +337,22 @@ def create_set_either_shm_region( if not (use_system_shared_memory or use_cuda_shared_memory): return [] - with CREATION_LOCK: - if use_cuda_shared_memory: - shm_ip_handle = cudashm.create_shared_memory_region( - shm_region_names[0] + "_data", input_byte_size, 0 - ) - shm_op_handle = cudashm.create_shared_memory_region( - shm_region_names[1] + "_data", output_byte_size, 0 - ) - cudashm.set_shared_memory_region(shm_ip_handle, input_list) - elif use_system_shared_memory: - shm_ip_handle = shm.create_shared_memory_region( - shm_region_names[0] + "_data", - "/" + shm_region_names[0], - input_byte_size, - ) - shm_op_handle = shm.create_shared_memory_region( - shm_region_names[1] + "_data", - "/" + shm_region_names[1], - output_byte_size, - ) - shm.set_shared_memory_region(shm_ip_handle, input_list) + if use_cuda_shared_memory: + shm_ip_handle = cudashm.create_shared_memory_region( + shm_region_names[0] + "_data", input_byte_size, 0 + ) + shm_op_handle = cudashm.create_shared_memory_region( + shm_region_names[1] + "_data", output_byte_size, 0 + ) + cudashm.set_shared_memory_region(shm_ip_handle, input_list) + elif use_system_shared_memory: + shm_ip_handle = shm.create_shared_memory_region( + shm_region_names[0] + "_data", "/" + shm_region_names[0], input_byte_size + ) + shm_op_handle = shm.create_shared_memory_region( + shm_region_names[1] + "_data", "/" + shm_region_names[1], output_byte_size + ) + shm.set_shared_memory_region(shm_ip_handle, input_list) return [shm_ip_handle, shm_op_handle] diff --git a/qa/common/test_util.py b/qa/common/test_util.py index 46a42668bf..f3bbcdb16f 100755 --- a/qa/common/test_util.py +++ b/qa/common/test_util.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -27,56 +27,15 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import json -import os import unittest -import ml_dtypes import numpy as np _last_request_id = 0 # Numpy does not support the BF16 datatype natively. -# We use ml_dtypes.bfloat16 for BF16. -np_dtype_bfloat16 = ml_dtypes.bfloat16 - -MIB = 2**20 # 1 MIB = 1,048,576 bytes -GIB = 2**30 # 1 GIB = 1,073,741,824 bytes - - -def get_server_process_from_env(env_var="SERVER_PID"): - """ - Return a psutil.Process for the tritonserver under test. - """ - import psutil - - pid_str = os.environ.get(env_var) - if not pid_str: - raise AssertionError(f"{env_var} env var is not set") - try: - return psutil.Process(int(pid_str)) - except (ValueError, psutil.NoSuchProcess) as e: - raise AssertionError(f"Invalid or stale {env_var}={pid_str!r}: {e}") - - -def wait_for_stable_rss(server, rss_tolerance_bytes=0.1 * MIB, stable_threshold=10): - """ - Wait until the RSS of the server is stable. - """ - import time - - last_rss = None - stable_count = 0 - while True: - rss = server.memory_info().rss - if last_rss is not None and abs(rss - last_rss) < rss_tolerance_bytes: - stable_count += 1 - else: - stable_count = 0 - last_rss = rss - if stable_count >= stable_threshold: - break - time.sleep(0.1) - return +# We use this dummy dtype as a representative for BF16. +np_dtype_bfloat16 = np.dtype([("bf16", object)]) def shape_element_count(shape): @@ -116,13 +75,7 @@ def shape_to_dims_str(shape): def validate_for_trt_model( - input_dtype, - output0_dtype, - output1_dtype, - # Unused arguments for consistency with validate_for_libtorch_model - input_shape=None, - output0_shape=None, - output1_shape=None, + input_dtype, output0_dtype, output1_dtype, input_shape, output0_shape, output1_shape ): """Return True if input and output dtypes are supported by a TRT model.""" supported_datatypes = [ @@ -160,6 +113,9 @@ def validate_for_ensemble_model( input_dtype, output0_dtype, output1_dtype, + input_shape, + output0_shape, + output1_shape, ): """Return True if input and output dtypes are supported by the ensemble type.""" @@ -192,13 +148,7 @@ def validate_for_ensemble_model( def validate_for_onnx_model( - input_dtype, - output0_dtype, - output1_dtype, - # Unused arguments for consistency with validate_for_libtorch_model - input_shape=None, - output0_shape=None, - output1_shape=None, + input_dtype, output0_dtype, output1_dtype, input_shape, output0_shape, output1_shape ): """Return True if input and output dtypes are supported by a Onnx model.""" @@ -277,10 +227,7 @@ def validate_for_libtorch_model( def validate_for_openvino_model( - input_dtype, - output0_dtype, - output1_dtype, - input_shape, + input_dtype, output0_dtype, output1_dtype, input_shape, output0_shape, output1_shape ): """Return True if input and output dtypes are supported by an OpenVino model.""" @@ -319,15 +266,12 @@ def get_dtype_name(dtype): def get_model_name(pf, input_dtype, output0_dtype, output1_dtype): - if output1_dtype is None: - return f"{pf}_{get_dtype_name(input_dtype)}_{get_dtype_name(output0_dtype)}" - else: - return "{}_{}_{}_{}".format( - pf, - get_dtype_name(input_dtype), - get_dtype_name(output0_dtype), - get_dtype_name(output1_dtype), - ) + return "{}_{}_{}_{}".format( + pf, + get_dtype_name(input_dtype), + get_dtype_name(output0_dtype), + get_dtype_name(output1_dtype), + ) def get_sequence_model_name(pf, dtype): @@ -365,37 +309,17 @@ def check_gpus_compute_capability(min_capability): Returns: bool """ + import pycuda.driver as cuda - import importlib.util - - if importlib.util.find_spec("cuda") is not None: - from cuda.core import Device - - devices = Device.get_all_devices() - for device in devices: - cc = device.compute_capability - compute_capability_value = cc.major + cc.minor / 10.0 - if compute_capability_value < min_capability: - return False - - elif importlib.util.find_spec("pycuda") is not None: - import pycuda.driver as cuda - - cuda.init() + cuda.init() - for device_index in range(cuda.Device.count()): - device = cuda.Device(device_index) - compute_capability = device.compute_capability() - compute_capability_value = ( - compute_capability[0] + compute_capability[1] / 10.0 - ) + for device_index in range(cuda.Device.count()): + device = cuda.Device(device_index) + compute_capability = device.compute_capability() + compute_capability_value = compute_capability[0] + compute_capability[1] / 10.0 - if compute_capability_value < min_capability: - return False - else: - raise RuntimeError( - "No packages found to determine the compute capability. Please check the environment." - ) + if compute_capability_value < min_capability: + return False return True diff --git a/qa/common/trtllm_util.sh b/qa/common/trtllm_util.sh deleted file mode 100755 index 81ecb2d770..0000000000 --- a/qa/common/trtllm_util.sh +++ /dev/null @@ -1,160 +0,0 @@ -#!/bin/bash -# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -function clone_tensorrt_llm_backend_repo { - rm -rf $TENSORRTLLM_BACKEND_DIR && mkdir $TENSORRTLLM_BACKEND_DIR - apt-get update && apt-get install git-lfs -y --no-install-recommends - git clone --single-branch --depth=1 -b ${TENSORRTLLM_BACKEND_REPO_TAG} ${TRITON_REPO_ORG}/tensorrtllm_backend.git $TENSORRTLLM_BACKEND_DIR - cd $TENSORRTLLM_BACKEND_DIR && git lfs install && git submodule update --init --recursive -} - -function build_gpt2_base_model { - # Download weights from HuggingFace Transformers - cd ${GPT_DIR} && rm -rf gpt2 && git clone https://huggingface.co/gpt2-medium gpt2 && cd gpt2 - rm pytorch_model.bin model.safetensors - if ! wget -q https://huggingface.co/gpt2-medium/resolve/main/pytorch_model.bin; then - echo "Downloading pytorch_model.bin failed." - exit 1 - fi - cd ${GPT_DIR} - - # Convert weights from HF Tranformers to FT format - python3 convert_checkpoint.py --model_dir gpt2 --dtype float16 --tp_size ${NUM_GPUS} --output_dir "./c-model/gpt2/${NUM_GPUS}-gpu/" - cd ${BASE_DIR} -} - -function build_gpt2_tensorrt_engine { - # Build TensorRT engines - cd ${GPT_DIR} - trtllm-build --checkpoint_dir "./c-model/gpt2/${NUM_GPUS}-gpu/" \ - --gpt_attention_plugin float16 \ - --remove_input_padding enable \ - --paged_kv_cache enable \ - --gemm_plugin float16 \ - --workers "${NUM_GPUS}" \ - --output_dir "${ENGINES_DIR}" - - cd ${BASE_DIR} -} - -function replace_config_tags { - tag_to_replace="${1}" - new_value="${2}" - config_file_path="${3}" - sed -i "s|${tag_to_replace}|${new_value}|g" ${config_file_path} -} - -function prepare_model_repository { - rm -rf ${MODEL_REPOSITORY} && mkdir ${MODEL_REPOSITORY} - cp -r ${TENSORRTLLM_BACKEND_DIR}/tensorrt_llm/triton_backend/all_models/inflight_batcher_llm/* ${MODEL_REPOSITORY} - rm -rf ${MODEL_REPOSITORY}/tensorrt_llm_bls - mv "${MODEL_REPOSITORY}/ensemble" "${MODEL_REPOSITORY}/${MODEL_NAME}" - - replace_config_tags "model_version: -1" "model_version: 1" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" - replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" - replace_config_tags 'name: "ensemble"' "name: \"$MODEL_NAME\"" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" - replace_config_tags '${logits_datatype}' "TYPE_FP32" "${MODEL_REPOSITORY}/${MODEL_NAME}/config.pbtxt" - - replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" - replace_config_tags '${preprocessing_instance_count}' '1' "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" - replace_config_tags '${tokenizer_dir}' "${TOKENIZER_DIR}/" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" - replace_config_tags '${logits_datatype}' "TYPE_FP32" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" - replace_config_tags '${max_queue_delay_microseconds}' "1000000" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" - replace_config_tags '${max_queue_size}' "0" "${MODEL_REPOSITORY}/preprocessing/config.pbtxt" - - replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" - replace_config_tags '${postprocessing_instance_count}' '1' "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" - replace_config_tags '${tokenizer_dir}' "${TOKENIZER_DIR}/" "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" - replace_config_tags '${logits_datatype}' "TYPE_FP32" "${MODEL_REPOSITORY}/postprocessing/config.pbtxt" - - replace_config_tags '${triton_max_batch_size}' "128" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" - replace_config_tags '${decoupled_mode}' 'true' "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" - replace_config_tags '${max_queue_delay_microseconds}' "1000000" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" - replace_config_tags '${batching_strategy}' 'inflight_fused_batching' "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" - replace_config_tags '${engine_dir}' "${ENGINES_DIR}" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" - replace_config_tags '${triton_backend}' "tensorrtllm" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" - replace_config_tags '${max_queue_size}' "0" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" - replace_config_tags '${logits_datatype}' "TYPE_FP32" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" - replace_config_tags '${encoder_input_features_data_type}' "TYPE_FP32" "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" - replace_config_tags '${prompt_embedding_table_data_type}' 'TYPE_FP16' "${MODEL_REPOSITORY}/tensorrt_llm/config.pbtxt" -} - -# Wait until server health endpoint shows ready. Sets WAIT_RET to 0 on -# success, 1 on failure -function wait_for_server_ready() { - local wait_time_secs="${1:-30}" - shift - local spids=("$@") - - WAIT_RET=0 - - for _ in $(seq "$wait_time_secs"); do - for pid in "${spids[@]}"; do - if ! kill -0 "$pid" >/dev/null 2>&1; then - echo "=== Server not running." - WAIT_RET=1 - return - fi - done - - sleep 1 - - if curl -s --fail localhost:8000/v2/health/ready && - curl -s --fail -w "%{http_code}" -o /dev/null -d '{"log_verbose_level":1}' localhost:8000/v2/logging; then - return - fi - done - - echo "=== Timeout $wait_time_secs secs. Server not ready." - WAIT_RET=1 -} - -function run_server { - python3 ${TENSORRTLLM_BACKEND_DIR}/tensorrt_llm/triton_backend/scripts/launch_triton_server.py --world_size="${NUM_GPUS}" --model_repo="${MODEL_REPOSITORY}" >${SERVER_LOG} 2>&1 & - sleep 2 # allow time to obtain the pid(s) - # Read PIDs into an array, trimming whitespaces - readarray -t SERVER_PID < <(pgrep "tritonserver") - - wait_for_server_ready ${SERVER_TIMEOUT} "${SERVER_PID[@]}" - if [ "$WAIT_RET" != "0" ]; then - # Cleanup - kill "${SERVER_PID[@]}" >/dev/null 2>&1 || true - echo -e "\n***\n*** Failed to start $SERVER\n***" - cat $SERVER_LOG - exit 1 - fi -} - -function kill_server { - pgrep tritonserver | xargs kill -SIGINT - for pid in "${SERVER_PID[@]}"; do - echo "Waiting for proc ${pid} to terminate..." - while kill -0 $pid >/dev/null 2>&1; do - sleep 1 - done - done -} diff --git a/qa/common/util.sh b/qa/common/util.sh index 08d10d7ed0..3874916573 100755 --- a/qa/common/util.sh +++ b/qa/common/util.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2018-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -196,12 +196,7 @@ function run_server () { echo "=== Running LD_PRELOAD=$SERVER_LD_PRELOAD $SERVER $SERVER_ARGS" fi - # If SERVER_ERROR_LOG is not set, redirect stderr to stdout - if [ -z "${SERVER_ERROR_LOG:-}" ]; then - LD_PRELOAD=$SERVER_LD_PRELOAD:${LD_PRELOAD} $SERVER $SERVER_ARGS > $SERVER_LOG 2>&1 & - else - LD_PRELOAD=$SERVER_LD_PRELOAD:${LD_PRELOAD} $SERVER $SERVER_ARGS > $SERVER_LOG 2>$SERVER_ERROR_LOG & - fi + LD_PRELOAD=$SERVER_LD_PRELOAD:${LD_PRELOAD} $SERVER $SERVER_ARGS > $SERVER_LOG 2>&1 & SERVER_PID=$! wait_for_server_ready $SERVER_PID $SERVER_TIMEOUT diff --git a/qa/python_models/bls_memory/model.py b/qa/python_models/bls_memory/model.py index a55da15774..69da4f440f 100644 --- a/qa/python_models/bls_memory/model.py +++ b/qa/python_models/bls_memory/model.py @@ -1,4 +1,4 @@ -# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -60,7 +60,7 @@ def test_bls_out_of_memory(self): input0_np, infer_response = self._send_identity_tensor( tensor_size, self._is_decoupled ) - out_of_memory_message = "Failed to increase the shared memory pool size" + out_of_memory_message = "Failed to increase the shared memory pool size for key" if infer_response.has_error(): self.assertIn(out_of_memory_message, infer_response.error().message()) diff --git a/qa/python_models/bls_memory_async/model.py b/qa/python_models/bls_memory_async/model.py index c96073eac0..d9e676b42e 100644 --- a/qa/python_models/bls_memory_async/model.py +++ b/qa/python_models/bls_memory_async/model.py @@ -1,4 +1,4 @@ -# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -53,7 +53,7 @@ async def test_bls_out_of_memory(): tensor_size = 256 * 1024 * 1024 input0_np, infer_response = await _send_identity_tensor(tensor_size, is_decoupled) - out_of_memory_message = "Failed to increase the shared memory pool size" + out_of_memory_message = "Failed to increase the shared memory pool size for key" if infer_response.has_error(): if not (out_of_memory_message in infer_response.error().message()): diff --git a/qa/python_models/decoupled_grpc_error/config.pbtxt b/qa/python_models/decoupled_grpc_error/config.pbtxt deleted file mode 100644 index 2a532b1f32..0000000000 --- a/qa/python_models/decoupled_grpc_error/config.pbtxt +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -backend: "python" -max_batch_size: 0 - -model_transaction_policy { - decoupled: true -} - -input [ - { - name: "IN" - data_type: TYPE_FP32 - dims: [ -1 ] - }, - { - name: "MODE" - data_type: TYPE_STRING - dims: [ 1 ] - } -] - -output [ - { - name: "OUT" - data_type: TYPE_FP32 - dims: [ -1 ] - } -] - -instance_group [ - { - count: 1 - kind : KIND_CPU - } -] diff --git a/qa/python_models/decoupled_grpc_error/model.py b/qa/python_models/decoupled_grpc_error/model.py deleted file mode 100644 index b498763923..0000000000 --- a/qa/python_models/decoupled_grpc_error/model.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import json -import threading -import time - -import triton_python_backend_utils as pb_utils - - -class TritonPythonModel: - """ - Decoupled model that always sends an error. - MODE input: - "MODE_ERROR_ONLY" (error response without FINAL flag), - "MODE_ERROR_FINAL" (error response with FINAL flag), - "MODE_ERROR_WITH_DELAYED_FINAL" (error response then delayed FINAL response). - Used to test gRPC server error handling behavior (e.g. triton_grpc_error stream closure and subsequent responses handling). - """ - - def initialize(self, args): - self.model_config = json.loads(args["model_config"]) - if not pb_utils.using_decoupled_model_transaction_policy(self.model_config): - raise pb_utils.TritonModelException( - "This model requires decoupled transaction policy" - ) - self.inflight_thread_count = 0 - self.inflight_thread_count_lck = threading.Lock() - - def execute(self, requests): - for request in requests: - in_tensor = pb_utils.get_input_tensor_by_name(request, "IN").as_numpy() - mode_tensor = pb_utils.get_input_tensor_by_name(request, "MODE").as_numpy() - mode = mode_tensor.flat[0].decode("utf-8") - thread = threading.Thread( - target=self._response_thread, - args=(request.get_response_sender(), in_tensor, mode), - ) - thread.daemon = True - with self.inflight_thread_count_lck: - self.inflight_thread_count += 1 - thread.start() - return None - - def _response_thread(self, response_sender, in_tensor, mode): - # Send a normal response first. - out_tensor = pb_utils.Tensor("OUT", in_tensor) - response = pb_utils.InferenceResponse(output_tensors=[out_tensor]) - response_sender.send(response) - - # Send an error response next. - error = pb_utils.TritonError("An error occurred during execution") - response = pb_utils.InferenceResponse(error=error) - - if mode == "MODE_ERROR_ONLY" or mode == "MODE_ERROR_WITH_DELAYED_FINAL": - response_sender.send(response) - elif mode == "MODE_ERROR_FINAL": - response_sender.send( - response, flags=pb_utils.TRITONSERVER_RESPONSE_COMPLETE_FINAL - ) - else: - raise ValueError(f"Invalid mode: {mode}") - - # Send a delayed FINAL flag. - if mode == "MODE_ERROR_WITH_DELAYED_FINAL": - time.sleep(0.5) - response_sender.send(flags=pb_utils.TRITONSERVER_RESPONSE_COMPLETE_FINAL) - - with self.inflight_thread_count_lck: - self.inflight_thread_count -= 1 - - def finalize(self): - inflight_threads = True - while inflight_threads: - with self.inflight_thread_count_lck: - inflight_threads = self.inflight_thread_count != 0 - if inflight_threads: - time.sleep(0.1) diff --git a/qa/python_models/identity_bf16/model.py b/qa/python_models/identity_bf16/model.py index d4ca2d250c..57756073b9 100644 --- a/qa/python_models/identity_bf16/model.py +++ b/qa/python_models/identity_bf16/model.py @@ -1,4 +1,4 @@ -# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -71,7 +71,7 @@ def execute(self, requests): bf16_dlpack = input_tensor.to_dlpack() # OPTIONAL: The tensor can be converted to other dlpack-compatible - # frameworks like PyTorch with their dlpack utilities. + # frameworks like PyTorch and TensorFlow with their dlpack utilities. torch_tensor = torch.utils.dlpack.from_dlpack(bf16_dlpack) # When complete, convert back to a pb_utils.Tensor via DLPack. diff --git a/qa/python_models/join_add_sub/config.pbtxt b/qa/python_models/join_add_sub/config.pbtxt deleted file mode 100644 index f1abd51ec3..0000000000 --- a/qa/python_models/join_add_sub/config.pbtxt +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -name: "join_add_sub" -backend: "python" -max_batch_size: 1 - -input [ - { - name: "INPUT0" - data_type: TYPE_FP32 - dims: [ 1 ] - } -] -input [ - { - name: "INPUT1" - data_type: TYPE_FP32 - dims: [ 1 ] - } -] -output [ - { - name: "OUTPUT0" - data_type: TYPE_FP32 - dims: [ 1 ] - } -] -output [ - { - name: "OUTPUT1" - data_type: TYPE_FP32 - dims: [ 1 ] - } -] - -instance_group [{ kind: KIND_CPU }] diff --git a/qa/python_models/join_add_sub/model.py b/qa/python_models/join_add_sub/model.py deleted file mode 100644 index 79371cf38f..0000000000 --- a/qa/python_models/join_add_sub/model.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import json - -import numpy as np -import triton_python_backend_utils as pb_utils - - -class TritonPythonModel: - def initialize(self, args): - self.model_config = model_config = json.loads(args["model_config"]) - - output0_config = pb_utils.get_output_config_by_name(model_config, "OUTPUT0") - output1_config = pb_utils.get_output_config_by_name(model_config, "OUTPUT1") - - self.output0_dtype = pb_utils.triton_string_to_numpy( - output0_config["data_type"] - ) - self.output1_dtype = pb_utils.triton_string_to_numpy( - output1_config["data_type"] - ) - - def execute(self, requests): - output0_dtype = self.output0_dtype - output1_dtype = self.output1_dtype - - responses = [] - for request in requests: - in_0 = pb_utils.get_input_tensor_by_name(request, "INPUT0") - in_1 = pb_utils.get_input_tensor_by_name(request, "INPUT1") - if ( - in_0.as_numpy().dtype.type is np.bytes_ - or in_0.as_numpy().dtype == np.object_ - ): - out_0, out_1 = ( - in_0.as_numpy().astype(np.int32) + in_1.as_numpy().astype(np.int32), - in_0.as_numpy().astype(np.int32) - in_1.as_numpy().astype(np.int32), - ) - else: - out_0, out_1 = ( - in_0.as_numpy() + in_1.as_numpy(), - in_0.as_numpy() - in_1.as_numpy(), - ) - - out_tensor_0 = pb_utils.Tensor("OUTPUT0", out_0.astype(output0_dtype)) - out_tensor_1 = pb_utils.Tensor("OUTPUT1", out_1.astype(output1_dtype)) - responses.append(pb_utils.InferenceResponse([out_tensor_0, out_tensor_1])) - return responses diff --git a/qa/python_models/python_version/model.py b/qa/python_models/python_version/model.py index d598681ae9..b1157ea50d 100644 --- a/qa/python_models/python_version/model.py +++ b/qa/python_models/python_version/model.py @@ -50,6 +50,8 @@ def initialize(self, args): self.model_config = args["model_config"] # This is to make sure that /bin/bash is not picking up # the wrong shared libraries after installing PyTorch. + # Tensorflow uses a shared library which is common with + # bash. os.system("/bin/bash --help") print( f"Python version is {sys.version_info.major}.{sys.version_info.minor}, NumPy version is {np.version.version}, and PyTorch version is {torch.__version__}", diff --git a/qa/python_models/response_sender_until_cancelled/config.pbtxt b/qa/python_models/response_sender_until_cancelled/config.pbtxt deleted file mode 100644 index 34a703db39..0000000000 --- a/qa/python_models/response_sender_until_cancelled/config.pbtxt +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -name: "response_sender_until_cancelled" -backend: "python" -model_transaction_policy { - decoupled: True -} - -input [ - { - name: "MAX_RESPONSE_COUNT" - data_type: TYPE_INT32 - dims: [ 1 ] - }, - { - name: "DELAY" - data_type: TYPE_INT32 - dims: [ 1 ] - }, - { - name: "INPUT" - data_type: TYPE_INT32 - dims: [ 1 ] - }, - { - name: "IGNORE_CANCEL" - data_type: TYPE_BOOL - dims: [ 1 ] - } -] -output [ - { - name: "OUTPUT" - data_type: TYPE_INT32 - dims: [ 1 ] - } -] - -instance_group [{ kind: KIND_CPU }] diff --git a/qa/python_models/response_sender_until_cancelled/model.py b/qa/python_models/response_sender_until_cancelled/model.py deleted file mode 100644 index c4ffe8dd32..0000000000 --- a/qa/python_models/response_sender_until_cancelled/model.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import time - -import numpy as np -import triton_python_backend_utils as pb_utils - - -class TritonPythonModel: - """This model will keep repeating the INPUT as the OUTPUT, - until the request is being cancelled or - the MAX_RESPONSE_COUNT has been reached. - """ - - def execute(self, requests): - request = requests[0] - - input = pb_utils.get_input_tensor_by_name(request, "INPUT").as_numpy() - max_response_count = pb_utils.get_input_tensor_by_name( - request, "MAX_RESPONSE_COUNT" - ).as_numpy()[0] - delay = pb_utils.get_input_tensor_by_name(request, "DELAY").as_numpy()[0] - ignore_cancel = pb_utils.get_input_tensor_by_name( - request, "IGNORE_CANCEL" - ).as_numpy()[0] - response_sender = request.get_response_sender() - - sent = 0 - while True: - if not ignore_cancel and request.is_cancelled(): - response = pb_utils.InferenceResponse( - error=pb_utils.TritonError( - message="request has been cancelled", - code=pb_utils.TritonError.CANCELLED, - ) - ) - response_sender.send( - response, flags=pb_utils.TRITONSERVER_RESPONSE_COMPLETE_FINAL - ) - break - - output = pb_utils.Tensor("OUTPUT", np.array([input[0]], np.int32)) - response = pb_utils.InferenceResponse(output_tensors=[output]) - - if sent + 1 == max_response_count: - response_sender.send( - response, flags=pb_utils.TRITONSERVER_RESPONSE_COMPLETE_FINAL - ) - break - else: - response_sender.send(response) - sent += 1 - time.sleep(delay / 1000) - - return None diff --git a/qa/python_models/torchvision/resnet50/model.py b/qa/python_models/torchvision/resnet50/model.py index 0e6405d302..6a31a77420 100644 --- a/qa/python_models/torchvision/resnet50/model.py +++ b/qa/python_models/torchvision/resnet50/model.py @@ -1,4 +1,4 @@ -# Copyright 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -27,7 +27,6 @@ import torch import triton_python_backend_utils as pb_utils from torch.utils.dlpack import to_dlpack -from torchvision import models class TritonPythonModel: @@ -36,8 +35,15 @@ def initialize(self, args): This function initializes pre-trained ResNet50 model. """ self.device = "cuda" if args["model_instance_kind"] == "GPU" else "cpu" + # Avoid the "HTTP Error 403: rate limit exceeded" error + torch.hub._validate_not_a_forked_repo = lambda a, b, c: True + # Our tests currently depend on torchvision=0.14, + # to make sure `torch.hub` loads Resnet50 implementation + # compatible with torchvision=0.14, we need to provide tag self.model = ( - models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2) + torch.hub.load( + "pytorch/vision:v0.14.1", "resnet50", weights="IMAGENET1K_V2" + ) .to(self.device) .eval() ) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index de46e970e4..8824e2ed6a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -24,7 +24,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -cmake_minimum_required (VERSION 3.31.8) +cmake_minimum_required (VERSION 3.18) project(tritonserverexe LANGUAGES C CXX) @@ -78,7 +78,7 @@ endif() # OpenTelemetry # -if (${TRITON_ENABLE_TRACING}) +if (NOT WIN32 AND ${TRITON_ENABLE_TRACING}) find_package(absl CONFIG REQUIRED) find_package(CURL CONFIG REQUIRED) find_package(nlohmann_json CONFIG REQUIRED) @@ -107,21 +107,53 @@ add_executable( triton_signal.h ) -set_property(TARGET main PROPERTY OUTPUT_NAME tritonserver) +# On windows a *.lib file can be generated for a exe. When creating +# tritonserver.exe if we try to create tritonserver.lib it will fail +# because there is already a trtionserver.lib for tritonserver.dll, +# this causes the build to fail. To avoid we keep the build name as +# main.exe and then for windows after installing we rename it to +# tritonserver.exe (below in the install steps). +if (NOT WIN32) + set_property(TARGET main PROPERTY OUTPUT_NAME tritonserver) +endif() target_compile_features(main PRIVATE cxx_std_${TRITON_MIN_CXX_STANDARD}) -target_compile_options( - main - PRIVATE - -Wall -Wextra -Wno-unused-parameter -Wno-deprecated-declarations -Werror -) +if(WIN32) + message("Using MSVC as compiler, default target on Windows 10. " + "If the target system is not Windows 10, please update _WIN32_WINNT " + "to corresponding value.") + target_compile_options( + main + PRIVATE + /W1 /D_WIN32_WINNT=0x0A00 /EHsc /Zc:preprocessor + ) + target_compile_definitions(main + PRIVATE + NOMINMAX) -# Dependency from common.h -target_link_libraries( - main - PRIVATE - b64 -) + # Dependency from common.h + find_library(B64_LIBRARY NAMES b64) + target_link_libraries( + main + PRIVATE + ${B64_LIBRARY} + ) + +else() + + target_compile_options( + main + PRIVATE + -Wall -Wextra -Wno-unused-parameter -Wno-deprecated-declarations -Werror + ) + + # Dependency from common.h + target_link_libraries( + main + PRIVATE + b64 + ) + endif() set(LIB_DIR "lib") if(LINUX) @@ -243,17 +275,29 @@ if(${TRITON_ENABLE_NVTX}) ) endif() # TRITON_ENABLE_NVTX -target_link_libraries( - main - PRIVATE - rt - dl -) +if (NOT WIN32) + target_link_libraries( + main + PRIVATE + rt + dl + ) +endif() # NOT WIN32 -install( - TARGETS main - RUNTIME DESTINATION bin -) +if (NOT WIN32) + install( + TARGETS main + RUNTIME DESTINATION bin + ) +else() + # See explanation above as to why we need to rename main.exe to + # tritonserver.exe as part of the install process on windows. + install( + PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_BUILD_TYPE}/main.exe + DESTINATION bin + RENAME tritonserver.exe + ) +endif() if(${TRITON_ENABLE_GRPC}) # @@ -330,11 +374,19 @@ if(${TRITON_ENABLE_HTTP} ) target_compile_features(http-endpoint-library PRIVATE cxx_std_${TRITON_MIN_CXX_STANDARD}) - target_compile_options( - http-endpoint-library - PRIVATE - -Wall -Wextra -Wno-unused-parameter -Wno-deprecated-declarations -Wno-error=maybe-uninitialized -Werror - ) + if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + target_compile_options( + http-endpoint-library + PRIVATE + /W1 /D_WIN32_WINNT=0x0A00 /EHsc /Zc:preprocessor + ) + else() + target_compile_options( + http-endpoint-library + PRIVATE + -Wall -Wextra -Wno-unused-parameter -Wno-deprecated-declarations -Wno-error=maybe-uninitialized -Werror + ) + endif() set_target_properties( http-endpoint-library @@ -359,7 +411,10 @@ if(${TRITON_ENABLE_HTTP} PRIVATE $ ) - if (${TRITON_ENABLE_TRACING}) + # FIXME when Triton support of Opentelemetry is available on Windows + # add ${OPENTELEMETRY_CPP_INCLUDE_DIRS} to above target_include_directories + # JIRA DLIS-4786 + if (NOT WIN32 AND ${TRITON_ENABLE_TRACING}) target_link_libraries( http-endpoint-library PRIVATE tracing-library @@ -436,12 +491,23 @@ if(${TRITON_ENABLE_HTTP} ) endif() # TRITON_ENABLE_NVTX - target_link_libraries( - http-endpoint-library - PUBLIC - b64 - z - ) + if (WIN32) + find_library(B64_LIBRARY NAMES b64) + find_library(ZLIB_LIBRARY NAMES zlib) + target_link_libraries( + http-endpoint-library + PUBLIC + ${B64_LIBRARY} + ${ZLIB_LIBRARY} + ) + else() + target_link_libraries( + http-endpoint-library + PUBLIC + b64 + z + ) + endif() target_link_libraries( main @@ -462,15 +528,18 @@ if(${TRITON_ENABLE_TRACING}) ) target_compile_features(tracing-library PRIVATE cxx_std_${TRITON_MIN_CXX_STANDARD}) - target_include_directories( - tracing-library - PUBLIC ${OPENTELEMETRY_CPP_INCLUDE_DIRS} - ) + # FIXME: remove, when Windows support is added for Opentelemetry + if (NOT WIN32) + target_include_directories( + tracing-library + PUBLIC ${OPENTELEMETRY_CPP_INCLUDE_DIRS} + ) - target_link_libraries( - tracing-library - PUBLIC - ${OPENTELEMETRY_CPP_LIBRARIES}) + target_link_libraries( + tracing-library + PUBLIC + ${OPENTELEMETRY_CPP_LIBRARIES}) + endif() set_target_properties( tracing-library @@ -541,130 +610,99 @@ if(${TRITON_ENABLE_TRACING}) ) endif() # TRITON_ENABLE_TRACING -# -# simple -# -add_executable( - simple - simple.cc -) - -target_compile_features(simple PRIVATE cxx_std_${TRITON_MIN_CXX_STANDARD}) -target_compile_options( - simple - PRIVATE - -Wall -Wextra -Wno-type-limits -Wno-unused-parameter -Wno-deprecated-declarations -Werror -) - -set_target_properties( - simple - PROPERTIES - POSITION_INDEPENDENT_CODE ON - SKIP_BUILD_RPATH TRUE - BUILD_WITH_INSTALL_RPATH TRUE - INSTALL_RPATH_USE_LINK_PATH FALSE - INSTALL_RPATH "" -) - -target_link_libraries( - simple - PRIVATE - triton-common-async-work-queue # from repo-common - triton-common-error # from repo-common - triton-core-serverapi # from repo-core - triton-core-serverstub # from repo-core +if (NOT WIN32) + # + # simple + # + add_executable( + simple + simple.cc ) -if(${TRITON_ENABLE_GPU}) - target_compile_definitions( + target_compile_features(simple PRIVATE cxx_std_${TRITON_MIN_CXX_STANDARD}) + if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + message("Using MSVC as compiler, default target on Windows 10. " + "If the target system is not Windows 10, please update _WIN32_WINNT " + "to corresponding value.") + target_compile_options( + simple + PRIVATE + /W1 /D_WIN32_WINNT=0x0A00 /EHsc /Zc:preprocessor + ) + else() + target_compile_options( + simple + PRIVATE + -Wall -Wextra -Wno-type-limits -Wno-unused-parameter -Wno-deprecated-declarations -Werror + ) + endif() + + set_target_properties( simple - PRIVATE TRITON_ENABLE_GPU=1 - PRIVATE TRITON_MIN_COMPUTE_CAPABILITY=${TRITON_MIN_COMPUTE_CAPABILITY} + PROPERTIES + POSITION_INDEPENDENT_CODE ON + SKIP_BUILD_RPATH TRUE + BUILD_WITH_INSTALL_RPATH TRUE + INSTALL_RPATH_USE_LINK_PATH FALSE + INSTALL_RPATH "" ) target_link_libraries( simple PRIVATE - CUDA::cudart - ) -endif() # TRITON_ENABLE_GPU - -install( - TARGETS simple - RUNTIME DESTINATION bin -) - -# -# multi_server example -# -add_executable( - multi_server - multi_server.cc -) - -target_compile_features(multi_server PRIVATE cxx_std_${TRITON_MIN_CXX_STANDARD}) -target_compile_options( - multi_server - PRIVATE - -Wall -Wextra -Wno-type-limits -Wno-unused-parameter -Wno-deprecated-declarations -Werror -) - -set_target_properties( - multi_server - PROPERTIES - POSITION_INDEPENDENT_CODE ON - SKIP_BUILD_RPATH TRUE - BUILD_WITH_INSTALL_RPATH TRUE - INSTALL_RPATH_USE_LINK_PATH FALSE - INSTALL_RPATH "" -) + triton-common-async-work-queue # from repo-common + triton-common-error # from repo-common + triton-core-serverapi # from repo-core + triton-core-serverstub # from repo-core + ) -target_link_libraries( - multi_server - PRIVATE - triton-common-async-work-queue # from repo-common - triton-common-error # from repo-common - triton-core-serverapi # from repo-core - triton-core-serverstub # from repo-core - ) + if(${TRITON_ENABLE_GPU}) + target_compile_definitions( + simple + PRIVATE TRITON_ENABLE_GPU=1 + PRIVATE TRITON_MIN_COMPUTE_CAPABILITY=${TRITON_MIN_COMPUTE_CAPABILITY} + ) -if(${TRITON_ENABLE_GPU}) - target_compile_definitions( - multi_server - PRIVATE TRITON_ENABLE_GPU=1 - PRIVATE TRITON_MIN_COMPUTE_CAPABILITY=${TRITON_MIN_COMPUTE_CAPABILITY} - ) + target_link_libraries( + simple + PRIVATE + CUDA::cudart + ) + endif() # TRITON_ENABLE_GPU - target_link_libraries( - multi_server - PRIVATE - CUDA::cudart + install( + TARGETS simple + RUNTIME DESTINATION bin ) -endif() # TRITON_ENABLE_GPU - -install( - TARGETS multi_server - RUNTIME DESTINATION bin -) -if(${TRITON_ENABLE_GPU}) # - # memory_alloc example + # multi_server example # add_executable( - memory_alloc - memory_alloc.cc + multi_server + multi_server.cc ) - target_compile_features(memory_alloc PRIVATE cxx_std_${TRITON_MIN_CXX_STANDARD}) - target_compile_options( - memory_alloc - PRIVATE - -Wall -Wextra -Wno-type-limits -Wno-unused-parameter -Wno-deprecated-declarations -Werror - ) + target_compile_features(multi_server PRIVATE cxx_std_${TRITON_MIN_CXX_STANDARD}) + if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + message("Using MSVC as compiler, default target on Windows 10. " + "If the target system is not Windows 10, please update _WIN32_WINNT " + "to corresponding value.") + target_compile_options( + multi_server + PRIVATE + /W1 /D_WIN32_WINNT=0x0A00 /EHsc /Zc:preprocessor + ) + else() + target_compile_options( + multi_server + PRIVATE + -Wall -Wextra -Wno-type-limits -Wno-unused-parameter -Wno-deprecated-declarations -Werror + ) + endif() set_target_properties( - memory_alloc + multi_server PROPERTIES POSITION_INDEPENDENT_CODE ON SKIP_BUILD_RPATH TRUE @@ -673,30 +711,102 @@ if(${TRITON_ENABLE_GPU}) INSTALL_RPATH "" ) - target_compile_definitions( - memory_alloc - PRIVATE TRITON_ENABLE_GPU=1 - PRIVATE TRITON_MIN_COMPUTE_CAPABILITY=${TRITON_MIN_COMPUTE_CAPABILITY} - ) - target_link_libraries( - memory_alloc + multi_server PRIVATE triton-common-async-work-queue # from repo-common triton-common-error # from repo-common triton-core-serverapi # from repo-core triton-core-serverstub # from repo-core - CUDA::cudart ) + if(${TRITON_ENABLE_GPU}) + target_compile_definitions( + multi_server + PRIVATE TRITON_ENABLE_GPU=1 + PRIVATE TRITON_MIN_COMPUTE_CAPABILITY=${TRITON_MIN_COMPUTE_CAPABILITY} + ) + + target_link_libraries( + multi_server + PRIVATE + CUDA::cudart + ) + endif() # TRITON_ENABLE_GPU + install( - TARGETS memory_alloc + TARGETS multi_server RUNTIME DESTINATION bin ) -endif() # TRITON_ENABLE_GPU -# tritonfrontend python package -add_subdirectory(python) + if(${TRITON_ENABLE_GPU}) + # + # memory_alloc example + # + add_executable( + memory_alloc + memory_alloc.cc + ) + + target_compile_features(memory_alloc PRIVATE cxx_std_${TRITON_MIN_CXX_STANDARD}) + if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + message("Using MSVC as compiler, default target on Windows 10. " + "If the target system is not Windows 10, please update _WIN32_WINNT " + "to corresponding value.") + target_compile_options( + memory_alloc + PRIVATE + /W1 /D_WIN32_WINNT=0x0A00 /EHsc /Zc:preprocessor + ) + else() + target_compile_options( + memory_alloc + PRIVATE + -Wall -Wextra -Wno-type-limits -Wno-unused-parameter -Wno-deprecated-declarations -Werror + ) + endif() + + set_target_properties( + memory_alloc + PROPERTIES + POSITION_INDEPENDENT_CODE ON + SKIP_BUILD_RPATH TRUE + BUILD_WITH_INSTALL_RPATH TRUE + INSTALL_RPATH_USE_LINK_PATH FALSE + INSTALL_RPATH "" + ) + + target_compile_definitions( + memory_alloc + PRIVATE TRITON_ENABLE_GPU=1 + PRIVATE TRITON_MIN_COMPUTE_CAPABILITY=${TRITON_MIN_COMPUTE_CAPABILITY} + ) + + target_link_libraries( + memory_alloc + PRIVATE + triton-common-async-work-queue # from repo-common + triton-common-error # from repo-common + triton-core-serverapi # from repo-core + triton-core-serverstub # from repo-core + CUDA::cudart + ) -add_subdirectory(test test) + install( + TARGETS memory_alloc + RUNTIME DESTINATION bin + ) + endif() # TRITON_ENABLE_GPU +endif() # NOT WIN32 + +# DLIS-7292: Extend tritonfrontend to build for Windows +if (NOT WIN32) + # tritonfrontend python package + add_subdirectory(python) +endif (NOT WIN32) + +# Currently unit tests do not build for windows... +if ( NOT WIN32) + add_subdirectory(test test) +endif() # NOT WIN32 diff --git a/src/classification.cc b/src/classification.cc index ca50ceb6dc..2d8cd26b9e 100644 --- a/src/classification.cc +++ b/src/classification.cc @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2026, NVIDIA CORPORATION. All rights reserved. +// Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -77,27 +77,8 @@ TopkClassifications( const TRITONSERVER_DataType datatype, const uint32_t req_class_count, std::vector* class_strs) { - const uint32_t dtype_byte_size = TRITONSERVER_DataTypeByteSize(datatype); - if (dtype_byte_size == 0) { - return TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INVALID_ARG, - std::string( - std::string("class result not available for output due to " - "unsupported type '") + - std::string(TRITONSERVER_DataTypeString(datatype)) + "'") - .c_str()); - } - - const size_t element_cnt = byte_size / dtype_byte_size; - // Prevent pathological memory / CPU usage from unbounded classification - // outputs. - constexpr size_t kMaxClassificationElements = 1'000'000; - - if (element_cnt > kMaxClassificationElements) { - return TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INVALID_ARG, - "classification output tensor too large"); - } + const size_t element_cnt = + byte_size / TRITONSERVER_DataTypeByteSize(datatype); switch (datatype) { case TRITONSERVER_TYPE_UINT8: diff --git a/src/command_line_parser.cc b/src/command_line_parser.cc index f228bcaf77..1a829e0992 100644 --- a/src/command_line_parser.cc +++ b/src/command_line_parser.cc @@ -1,4 +1,4 @@ -// Copyright 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -298,7 +298,6 @@ enum TritonOptionId { OPTION_HTTP_ADDRESS, OPTION_HTTP_THREAD_COUNT, OPTION_HTTP_RESTRICTED_API, - OPTION_HTTP_MAX_INPUT_SIZE, #endif // TRITON_ENABLE_HTTP #if defined(TRITON_ENABLE_GRPC) OPTION_ALLOW_GRPC, @@ -306,7 +305,6 @@ enum TritonOptionId { OPTION_REUSE_GRPC_PORT, OPTION_GRPC_ADDRESS, OPTION_GRPC_HEADER_FORWARD_PATTERN, - OPTION_GRPC_INFER_THREAD_COUNT, OPTION_GRPC_INFER_ALLOCATION_POOL_SIZE, OPTION_GRPC_MAX_RESPONSE_POOL_SIZE, OPTION_GRPC_USE_SSL, @@ -377,8 +375,7 @@ enum TritonOptionId { OPTION_HOST_POLICY, OPTION_MODEL_LOAD_GPU_LIMIT, OPTION_MODEL_NAMESPACING, - OPTION_ENABLE_PEER_ACCESS, - OPTION_ALLOW_CLIENT_SHM + OPTION_ENABLE_PEER_ACCESS }; void @@ -425,11 +422,11 @@ TritonParser::SetupOptions() "Specify the mode for model management. Options are \"none\", \"poll\" " "and \"explicit\". The default is \"none\". " "For \"none\", the server will load all models in the model " - "repository(s) at startup and will not make any changes to the loaded " + "repository(s) at startup and will not make any changes to the load " "models after that. For \"poll\", the server will poll the model " "repository(s) to detect changes and will load/unload models based on " "those changes. The poll rate is controlled by 'repository-poll-secs'. " - "For \"explicit\", model load and unload are initiated by using the " + "For \"explicit\", model load and unload is initiated by using the " "model control APIs, and only models specified with --load-model will " "be loaded at startup."}); model_repo_options_.push_back( @@ -499,16 +496,11 @@ TritonParser::SetupOptions() http_options_.push_back( {OPTION_HTTP_THREAD_COUNT, "http-thread-count", Option::ArgInt, "Number of threads handling HTTP requests."}); - http_options_.push_back( - {OPTION_HTTP_MAX_INPUT_SIZE, "http-max-input-size", Option::ArgInt, - ("Maximum allowed HTTP request input size in bytes. For compressed " - "requests, this also limits the decompressed size. Default is " + - std::to_string(HTTP_DEFAULT_MAX_INPUT_SIZE) + " bytes (64MB).")}); http_options_.push_back( {OPTION_HTTP_RESTRICTED_API, "http-restricted-api", ":=", "Specify restricted HTTP api setting. The format of this " - "flag is --http-restricted-api=:=. Where " + "flag is --http-restricted-api=,=. Where " " is a comma-separated list of apis to be restricted. " " will be additional header key to be checked when a HTTP request " "is received, and is the value expected to be matched." @@ -538,10 +530,6 @@ TritonParser::SetupOptions() Option::ArgStr, "The regular expression pattern that will be used for forwarding GRPC " "headers as inference request parameters."}); - grpc_options_.push_back( - {OPTION_GRPC_INFER_THREAD_COUNT, "grpc-infer-thread-count", - Option::ArgInt, - "The number of gRPC inference handler threads. Default is 2."}); grpc_options_.push_back( {OPTION_GRPC_INFER_ALLOCATION_POOL_SIZE, "grpc-infer-allocation-pool-size", Option::ArgInt, @@ -632,7 +620,7 @@ TritonParser::SetupOptions() {OPTION_GRPC_RESTRICTED_PROTOCOL, "grpc-restricted-protocol", ":=", "Specify restricted GRPC protocol setting. The format of this " - "flag is --grpc-restricted-protocol=:=. Where " + "flag is --grpc-restricted-protocol=,=. Where " " is a comma-separated list of protocols to be restricted. " " will be additional header key to be checked when a GRPC request " "is received, and is the value expected to be matched." @@ -849,13 +837,6 @@ TritonParser::SetupOptions() "limit, the load will be rejected. If not specified, the limit will " "not be set."}); - shared_memory_options_.push_back( - {OPTION_ALLOW_CLIENT_SHM, "allow-client-shm", Option::ArgBool, - "Allow clients to register/unregister and use shared memory regions " - "(both CPU system shared memory and GPU CUDA IPC shared memory) for " - "inference inputs and outputs. Internal shared memory used by backends " - "such as the Python backend is unaffected. Default is false."}); - backend_options_.push_back( {OPTION_BACKEND_DIR, "backend-directory", Option::ArgStr, "The global directory searched for backend shared libraries. Default is " @@ -938,7 +919,6 @@ TritonParser::SetupOptionGroups() option_groups_.emplace_back("Rate Limiter", rate_limiter_options_); option_groups_.emplace_back( "Memory/Device Management", memory_device_options_); - option_groups_.emplace_back("Shared Memory", shared_memory_options_); option_groups_.emplace_back("DEPRECATED", deprecated_options_); } @@ -1408,15 +1388,6 @@ TritonParser::Parse(int argc, char** argv) case OPTION_HTTP_THREAD_COUNT: lparams.http_thread_cnt_ = ParseOption(optarg); break; - case OPTION_HTTP_MAX_INPUT_SIZE: { - int64_t temp_input_size = ParseOption(optarg); - if (temp_input_size <= 0) { - throw ParseException( - "Error: --http-max-input-size must be greater than 0."); - } - lparams.http_max_input_size_ = temp_input_size; - break; - } case OPTION_HTTP_RESTRICTED_API: ParseRestrictedFeatureOption( optarg, long_options[option_index].name, "", "api", @@ -1470,15 +1441,6 @@ TritonParser::Parse(int argc, char** argv) case OPTION_GRPC_ADDRESS: lgrpc_options.socket_.address_ = optarg; break; - case OPTION_GRPC_INFER_THREAD_COUNT: - lgrpc_options.infer_thread_count_ = ParseOption(optarg); - if (lgrpc_options.infer_thread_count_ < 2 || - lgrpc_options.infer_thread_count_ > 128) { - throw ParseException( - "invalid argument for --grpc_infer_thread_count. Must be in " - "the range 2 to 128."); - } - break; case OPTION_GRPC_INFER_ALLOCATION_POOL_SIZE: lgrpc_options.infer_allocation_pool_size_ = ParseOption(optarg); break; @@ -1790,9 +1752,6 @@ TritonParser::Parse(int argc, char** argv) case OPTION_ENABLE_PEER_ACCESS: lparams.enable_peer_access_ = ParseOption(optarg); break; - case OPTION_ALLOW_CLIENT_SHM: - lparams.allow_client_shm_ = ParseOption(optarg); - break; } } catch (const ParseException& pe) { diff --git a/src/command_line_parser.h b/src/command_line_parser.h index c7effe8960..762ee87b6d 100644 --- a/src/command_line_parser.h +++ b/src/command_line_parser.h @@ -1,4 +1,4 @@ -// Copyright 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2022-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -35,7 +35,6 @@ #include #include -#include "common.h" #include "restricted_features.h" #include "triton/common/logging.h" #include "triton/core/tritonserver.h" @@ -198,8 +197,6 @@ struct TritonServerParameters { // The number of threads to initialize for the HTTP front-end. int http_thread_cnt_{8}; RestrictedFeatures http_restricted_apis_{}; - // Default value 64MB - size_t http_max_input_size_{HTTP_DEFAULT_MAX_INPUT_SIZE}; #endif // TRITON_ENABLE_HTTP #ifdef TRITON_ENABLE_GRPC @@ -240,9 +237,6 @@ struct TritonServerParameters { std::string vertex_ai_default_model_{}; #endif // TRITON_ENABLE_VERTEX_AI - // Shared memory access control - bool allow_client_shm_{false}; - // [FIXME] who should call this function? void CheckPortCollision(); using ManagedTritonServerOptionPtr = std::unique_ptr< @@ -353,7 +347,6 @@ class TritonParser { std::vector

dir_ptr{dir, closedir}; + if ((dir = opendir(repo_path.c_str())) != NULL) { while ((ent = readdir(dir)) != NULL) { if ((ent->d_type == DT_DIR) && (!strcmp(ent->d_name, ".") == 0) && (!strcmp(ent->d_name, "..") == 0)) { @@ -980,7 +988,7 @@ SagemakerAPIServer::SageMakerMMELoadModel( // Read the config.pbtxt file at each path, if available std::string ensemble_config_path = - url_abspath + "/" + model_subdir + "/config.pbtxt"; + repo_path + "/" + model_subdir + "/" + "config.pbtxt"; std::ifstream config_fstream(ensemble_config_path); std::stringstream ensemble_config_content; @@ -1011,6 +1019,7 @@ SagemakerAPIServer::SageMakerMMELoadModel( << std::endl; } } + closedir(dir); } if (!strcmp(ensemble_model_subdir.c_str(), "") == 0) { @@ -1027,20 +1036,11 @@ SagemakerAPIServer::SageMakerMMELoadModel( */ std::string repo_parent_path, subdir, customer_subdir; - bool matched = RE2::FullMatch( - url_abspath, model_path_regex_, &repo_parent_path, &subdir, + RE2::FullMatch( + repo_path, model_path_regex_, &repo_parent_path, &subdir, &customer_subdir); - if (!matched) { - HTTP_RESPOND_IF_ERR( - req, TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INVALID_ARG, - ("Invalid \"url\" '" + url_string + - "': expected a path of the form " - "/opt/ml/models//model[/]") - .c_str())); - } - std::string config_path = url_abspath + "/config.pbtxt"; + std::string config_path = repo_path + "/config.pbtxt"; struct stat buffer; /* If config.pbtxt is at repo root, @@ -1051,7 +1051,7 @@ SagemakerAPIServer::SageMakerMMELoadModel( if (stat(config_path.c_str(), &buffer) == 0) { model_subdir = subdir; } else { - repo_parent_path = url_abspath; + repo_parent_path = repo_path; } auto param = TRITONSERVER_ParameterNew( diff --git a/src/sagemaker_server.h b/src/sagemaker_server.h index f4392e67b6..ffb94322df 100644 --- a/src/sagemaker_server.h +++ b/src/sagemaker_server.h @@ -1,4 +1,4 @@ -// Copyright 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -23,7 +23,6 @@ // OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - #pragma once #include @@ -46,7 +45,6 @@ class SagemakerAPIServer : public HTTPAPIServer { triton::server::TraceManager* trace_manager, const std::shared_ptr& smb_manager, const int32_t port, const std::string address, const int thread_cnt, - const size_t max_input_size, const RestrictedFeatures& restricted_apis, std::unique_ptr* sagemaker_server); class SagemakeInferRequestClass : public InferRequestClass { @@ -55,10 +53,12 @@ class SagemakerAPIServer : public HTTPAPIServer { TRITONSERVER_Server* server, evhtp_request_t* req, DataCompressor::Type response_compression_type, const std::shared_ptr& triton_request, - const std::shared_ptr& shm_manager) + const std::shared_ptr& shm_manager, + bool pause_http_request = true, + bool register_fini_cancel_hook = true) : InferRequestClass( server, req, response_compression_type, triton_request, - shm_manager) + shm_manager, pause_http_request, register_fini_cancel_hook) { } using InferRequestClass::InferResponseComplete; @@ -75,12 +75,10 @@ class SagemakerAPIServer : public HTTPAPIServer { const std::shared_ptr& server, triton::server::TraceManager* trace_manager, const std::shared_ptr& shm_manager, - const int32_t port, const std::string address, const int thread_cnt, - const size_t max_input_size, const RestrictedFeatures& restricted_apis) + const int32_t port, const std::string address, const int thread_cnt) : HTTPAPIServer( server, trace_manager, shm_manager, port, false /* reuse_port */, - address, "" /* header_forward_pattern */, thread_cnt, - max_input_size, restricted_apis), + address, "" /* header_forward_pattern */, thread_cnt), ping_regex_(R"(/ping)"), invocations_regex_(R"(/invocations)"), models_regex_(R"(/models(?:/)?([^/]+)?(/invoke)?)"), model_path_regex_( @@ -127,12 +125,13 @@ class SagemakerAPIServer : public HTTPAPIServer { std::unique_ptr CreateInferRequest( evhtp_request_t* req, - const std::shared_ptr& triton_request) - override + const std::shared_ptr& triton_request, + bool pause_http_request = true, + bool register_fini_cancel_hook = true) override { return std::unique_ptr(new SagemakeInferRequestClass( server_.get(), req, GetResponseCompressionType(req), triton_request, - shm_manager_)); + shm_manager_, pause_http_request, register_fini_cancel_hook)); } TRITONSERVER_Error* GetInferenceHeaderLength( evhtp_request_t* req, int32_t content_length, diff --git a/src/shared_memory_manager.cc b/src/shared_memory_manager.cc index cacc84b083..e1966f79c8 100644 --- a/src/shared_memory_manager.cc +++ b/src/shared_memory_manager.cc @@ -1,4 +1,4 @@ -// Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -354,10 +354,7 @@ SharedMemoryManager::RegisterSystemSharedMemory( const std::string& name, const std::string& shm_key, const size_t offset, const size_t byte_size) { - // Check if the shared memory key starts with the reserved prefix - RETURN_IF_ERR(ValidateSharedMemoryKey(name, shm_key)); - - std::lock_guard lock{mu_}; + std::lock_guard lock(mu_); if (shared_memory_map_.find(name) != shared_memory_map_.end()) { return TRITONSERVER_ErrorNew( @@ -367,10 +364,23 @@ SharedMemoryManager::RegisterSystemSharedMemory( } // register - void* mapped_addr{nullptr}; - int shm_fd{-1}; + void* mapped_addr; + int shm_fd = -1; + + // don't re-open if shared memory is already open + for (auto itr = shared_memory_map_.begin(); itr != shared_memory_map_.end(); + ++itr) { + if (itr->second->shm_key_ == shm_key) { + // FIXME: Consider invalid file descriptors after close + shm_fd = itr->second->shm_fd_; + break; + } + } - RETURN_IF_ERR(OpenSharedMemoryRegion(shm_key, &shm_fd)); + // open and set new shm_fd if new shared memory key + if (shm_fd == -1) { + RETURN_IF_ERR(OpenSharedMemoryRegion(shm_key, &shm_fd)); + } // Enforce that registered region is in-bounds of shm file object. RETURN_IF_ERR( @@ -413,7 +423,7 @@ SharedMemoryManager::RegisterCUDASharedMemory( const size_t byte_size, const int device_id) { // Serialize all operations that write/read current shared memory regions - std::lock_guard lock{mu_}; + std::lock_guard lock(mu_); // If name is already in shared_memory_map_ then return error saying already // registered @@ -462,32 +472,22 @@ SharedMemoryManager::GetMemoryInfo( } // validate offset - size_t shm_region_size = 0; + size_t shm_region_end = 0; + if (it->second->kind_ == TRITONSERVER_MEMORY_CPU) { + shm_region_end = it->second->offset_; + } if (it->second->byte_size_ > 0) { - shm_region_size += it->second->byte_size_; + shm_region_end += it->second->byte_size_ - 1; } - if (offset >= shm_region_size) { + if (offset > shm_region_end) { return TRITONSERVER_ErrorNew( TRITONSERVER_ERROR_INVALID_ARG, std::string("Invalid offset for shared memory region: '" + name + "'") .c_str()); } - - // Check for potential integer overflow before validating bounds - if (byte_size > (SIZE_MAX - offset)) { - return TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INVALID_ARG, - std::string( - "Integer overflow detected: byte_size (" + - std::to_string(byte_size) + ") + offset (" + - std::to_string(offset) + ") exceeds maximum value (" + - std::to_string(SIZE_MAX) + ") for region '" + name + "'") - .c_str()); - } - // validate byte_size + offset is within memory bounds - size_t total_req_shm = offset + byte_size; - if (total_req_shm > shm_region_size) { + size_t total_req_shm = offset + byte_size - 1; + if (total_req_shm > shm_region_end) { return TRITONSERVER_ErrorNew( TRITONSERVER_ERROR_INVALID_ARG, std::string( @@ -500,7 +500,12 @@ SharedMemoryManager::GetMemoryInfo( *shm_info = std::static_pointer_cast(it->second); } - *shm_mapped_addr = (void*)((uint8_t*)it->second->mapped_addr_ + offset); + if (it->second->kind_ == TRITONSERVER_MEMORY_CPU) { + *shm_mapped_addr = (void*)((uint8_t*)it->second->mapped_addr_ + + it->second->offset_ + offset); + } else { + *shm_mapped_addr = (void*)((uint8_t*)it->second->mapped_addr_ + offset); + } *memory_type = it->second->kind_; *device_id = it->second->device_id_; @@ -652,6 +657,7 @@ SharedMemoryManager::UnregisterAll(TRITONSERVER_MemoryType memory_type) ++next_it; if (it->second->kind_ == TRITONSERVER_MEMORY_GPU) { TRITONSERVER_Error* err = UnregisterHelper(it->first, memory_type); + ; if (err != nullptr) { unregister_fails.push_back(it->first); LOG_VERBOSE(1) << TRITONSERVER_ErrorMessage(err); diff --git a/src/shared_memory_manager.h b/src/shared_memory_manager.h index 6dde53b674..0f13f8e167 100644 --- a/src/shared_memory_manager.h +++ b/src/shared_memory_manager.h @@ -1,4 +1,4 @@ -// Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -47,12 +47,7 @@ namespace triton { namespace server { class SharedMemoryManager { public: - explicit SharedMemoryManager(bool allow_client_shm) - : allow_client_shm_(allow_client_shm) - { - } - SharedMemoryManager() = delete; - SharedMemoryManager(const SharedMemoryManager&) = delete; + SharedMemoryManager() = default; ~SharedMemoryManager(); /// A struct that records the shared memory regions registered by the shared @@ -64,8 +59,8 @@ class SharedMemoryManager { void* mapped_addr, const TRITONSERVER_MemoryType kind, const int64_t device_id) : name_(name), shm_key_(shm_key), offset_(offset), - byte_size_(byte_size), mapped_addr_(mapped_addr), kind_(kind), - device_id_(device_id), awaiting_unregister_(false) + byte_size_(byte_size), shm_fd_(shm_fd), mapped_addr_(mapped_addr), + kind_(kind), device_id_(device_id), awaiting_unregister_(false) { } @@ -73,6 +68,7 @@ class SharedMemoryManager { std::string shm_key_; size_t offset_; size_t byte_size_; + int shm_fd_; void* mapped_addr_; TRITONSERVER_MemoryType kind_; int64_t device_id_; @@ -174,11 +170,6 @@ class SharedMemoryManager { const std::string& name, TRITONSERVER_MemoryType memory_type, triton::common::TritonJson::Value* shm_status); - /// Returns whether register/unregister of system/CUDA shared memory (client - /// shared memory) is allowed. - /// \return boolean value indicating whether client shared memory is allowed. - inline bool AllowClientSharedMemory() const { return allow_client_shm_; } - /// Removes the named shared memory block of the specified type from /// the manager. Any future attempt to get the details of this block /// will result in an array till another block with the same name is @@ -207,8 +198,5 @@ class SharedMemoryManager { SharedMemoryStateMap shared_memory_map_; // A mutex to protect the concurrent access to shared_memory_map_ std::mutex mu_; - - // Flag to allow clients to register shared memory. - const bool allow_client_shm_; }; }} // namespace triton::server diff --git a/src/simple.cc b/src/simple.cc index cf0cafeed5..c429861cc5 100644 --- a/src/simple.cc +++ b/src/simple.cc @@ -1,4 +1,4 @@ -// Copyright 2020-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2020-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -876,10 +876,6 @@ main(int argc, char** argv) FAIL_IF_ERR( TRITONSERVER_InferenceResponseDelete(completed_response), "deleting inference response"); - - // We need to make sure that the previous request was released before - // reusing it. - request_release_future.get(); } // The TRITONSERVER_InferenceRequest object can be reused for @@ -910,6 +906,10 @@ main(int argc, char** argv) InferResponseComplete, reinterpret_cast(p)), "setting response callback"); + // We need to make sure that the previous request was released before + // reusing it. + request_release_future.get(); + // Register a new promise for the request callback barrier. barrier = std::make_unique>(); request_release_future = barrier->get_future(); @@ -936,10 +936,6 @@ main(int argc, char** argv) FAIL_IF_ERR( TRITONSERVER_InferenceResponseDelete(completed_response), "deleting inference response"); - - // We need to make sure that the previous request was released before - // reusing it. - request_release_future.get(); } // There are other TRITONSERVER_InferenceRequest APIs that allow @@ -971,7 +967,7 @@ main(int argc, char** argv) // Register a new promise for the request callback barrier. barrier = std::make_unique>(); - request_release_future = barrier->get_future(); + request_release_future.get(); FAIL_IF_ERR( TRITONSERVER_InferenceRequestSetReleaseCallback( @@ -997,10 +993,6 @@ main(int argc, char** argv) FAIL_IF_ERR( TRITONSERVER_InferenceResponseDelete(completed_response), "deleting inference response"); - - // Make sure the request is released before deleting it. If not, release - // callback will segfault after barrier is destructed. - request_release_future.get(); } FAIL_IF_ERR( diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index f523c4dbdc..ab8686919d 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -24,7 +24,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -cmake_minimum_required (VERSION 3.31.8) +cmake_minimum_required (VERSION 3.18) # # Unit tests @@ -77,55 +77,6 @@ if(${TRITON_ENABLE_HTTP} OR ${TRITON_ENABLE_METRICS} OR ) endif() -# -# Unit test for Backend + Common + Core tensor size APIs (GetElementCount, -# GetByteSize). Core template headers are used directly; the few core symbols -# not available via libtritonserver.so (hidden by linker script) are defined -# in the test source. -# -if(TARGET proto-library) - add_executable( - tensor_size_test - tensor_size_test.cc - ) - - set_target_properties( - tensor_size_test - PROPERTIES - SKIP_BUILD_RPATH TRUE - BUILD_WITH_INSTALL_RPATH TRUE - INSTALL_RPATH_USE_LINK_PATH FALSE - INSTALL_RPATH "$ORIGIN/../lib" - ) - - target_include_directories( - tensor_size_test - PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/.. - ${GTEST_INCLUDE_DIRS} - ${repo-core_SOURCE_DIR}/src - $ - ) - - target_link_libraries( - tensor_size_test - PRIVATE - triton-backend-utils - triton-core-serverapi - triton-core-serverstub - triton-common-model-config - proto-library - protobuf::libprotobuf - GTest::gtest - GTest::gtest_main - ) - - install( - TARGETS tensor_size_test - RUNTIME DESTINATION bin - ) -endif() - add_subdirectory(repoagent/relocation_repoagent repoagent/relocation_repoagent) add_subdirectory(distributed_addsub distributed_addsub) diff --git a/src/test/data_compressor_test.cc b/src/test/data_compressor_test.cc index 455bcd3468..292c8c544a 100644 --- a/src/test/data_compressor_test.cc +++ b/src/test/data_compressor_test.cc @@ -1,4 +1,4 @@ -// Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2021-2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -51,17 +51,6 @@ namespace ni = triton::server; namespace { -// Size Constants -constexpr size_t KB = 1024; // 1 KB = 1,024 bytes -constexpr size_t MB = 1024 * KB; // 1 MB = 1,048,576 bytes - -// Triton's default HTTP max input size -constexpr size_t DEFAULT_MAX_INPUT_SIZE = 64 * MB; // 64 MB - -// Test data sizes relative to the limit -constexpr size_t UNDER_LIMIT_DATA_SIZE = DEFAULT_MAX_INPUT_SIZE - MB; // 63 MB -constexpr size_t OVER_LIMIT_DATA_SIZE = DEFAULT_MAX_INPUT_SIZE + MB; // 65 MB - struct TritonServerError { TritonServerError(TRITONSERVER_Error_Code code, const char* msg) : code_(code), msg_(msg) @@ -75,12 +64,11 @@ void WriteEVBufferToFile(const std::string& file_name, evbuffer* evb) { std::ofstream fs(file_name); - std::unique_ptr buffer_array_holder; struct evbuffer_iovec* buffer_array = nullptr; int buffer_count = evbuffer_peek(evb, -1, NULL, NULL, 0); if (buffer_count > 0) { - buffer_array_holder.reset(new struct evbuffer_iovec[buffer_count]); - buffer_array = buffer_array_holder.get(); + buffer_array = static_cast( + alloca(sizeof(struct evbuffer_iovec) * buffer_count)); ASSERT_EQ( evbuffer_peek(evb, -1, NULL, buffer_array, buffer_count), buffer_count) << "unexpected error getting buffers for result"; @@ -97,12 +85,11 @@ EVBufferToContiguousBuffer(evbuffer* evb, std::vector* buffer) { *buffer = std::vector(evbuffer_get_length(evb)); { - std::unique_ptr buffer_array_holder; struct evbuffer_iovec* buffer_array = nullptr; int buffer_count = evbuffer_peek(evb, -1, NULL, NULL, 0); if (buffer_count > 0) { - buffer_array_holder.reset(new struct evbuffer_iovec[buffer_count]); - buffer_array = buffer_array_holder.get(); + buffer_array = static_cast( + alloca(sizeof(struct evbuffer_iovec) * buffer_count)); ASSERT_EQ( evbuffer_peek(evb, -1, NULL, buffer_array, buffer_count), buffer_count) @@ -289,12 +276,11 @@ TEST_F(DataCompressorTest, DeflateTwoBuffer) << "Failed to create compressed evbuffer"; // Reconstruct the compressed buffer to be two buffers if (evbuffer_peek(compressed, -1, NULL, NULL, 0) == 1) { - std::unique_ptr buffer_array_holder; struct evbuffer_iovec* buffer_array = nullptr; int buffer_count = evbuffer_peek(compressed, -1, NULL, NULL, 0); if (buffer_count > 0) { - buffer_array_holder.reset(new struct evbuffer_iovec[buffer_count]); - buffer_array = buffer_array_holder.get(); + buffer_array = static_cast( + alloca(sizeof(struct evbuffer_iovec) * buffer_count)); ASSERT_EQ( evbuffer_peek(compressed, -1, NULL, buffer_array, buffer_count), buffer_count) @@ -371,12 +357,11 @@ TEST_F(DataCompressorTest, GzipTwoBuffer) << "Failed to create compressed evbuffer"; // Reconstruct the compressed buffer to be two buffers if (evbuffer_peek(compressed, -1, NULL, NULL, 0) == 1) { - std::unique_ptr buffer_array_holder; struct evbuffer_iovec* buffer_array = nullptr; int buffer_count = evbuffer_peek(compressed, -1, NULL, NULL, 0); if (buffer_count > 0) { - buffer_array_holder.reset(new struct evbuffer_iovec[buffer_count]); - buffer_array = buffer_array_holder.get(); + buffer_array = static_cast( + alloca(sizeof(struct evbuffer_iovec) * buffer_count)); ASSERT_EQ( evbuffer_peek(compressed, -1, NULL, buffer_array, buffer_count), buffer_count) @@ -631,187 +616,6 @@ TEST_F(DataCompressorTest, CompressGzipBuffer) WriteEVBufferToFile("generated_gzip_compressed_data", compressed); } -// Helper to compress data with GZIP and return the compressed evbuffer -evbuffer* -CompressWithGzip(const char* data, size_t size) -{ - auto source = evbuffer_new(); - evbuffer_add(source, data, size); - auto compressed = evbuffer_new(); - ni::DataCompressor::CompressData( - ni::DataCompressor::Type::GZIP, source, compressed); - evbuffer_free(source); - return compressed; -} - -// Helper to compress data with DEFLATE and return the compressed evbuffer -evbuffer* -CompressWithDeflate(const char* data, size_t size) -{ - auto source = evbuffer_new(); - evbuffer_add(source, data, size); - auto compressed = evbuffer_new(); - ni::DataCompressor::CompressData( - ni::DataCompressor::Type::DEFLATE, source, compressed); - evbuffer_free(source); - return compressed; -} - -// This test verifies that the max_decompressed_size parameter correctly -// limits the memory allocation during GZIP decompression. -TEST_F(DataCompressorTest, DecompressionSizeLimitGzip) -{ - // Create test data buffers of different sizes - std::unique_ptr under_data(new char[UNDER_LIMIT_DATA_SIZE]); - std::unique_ptr at_data(new char[DEFAULT_MAX_INPUT_SIZE]); - std::unique_ptr over_data(new char[OVER_LIMIT_DATA_SIZE]); - memset(under_data.get(), 'A', UNDER_LIMIT_DATA_SIZE); - memset(at_data.get(), 'B', DEFAULT_MAX_INPUT_SIZE); - memset(over_data.get(), 'C', OVER_LIMIT_DATA_SIZE); - - // Compress each data set - auto under_compressed = - CompressWithGzip(under_data.get(), UNDER_LIMIT_DATA_SIZE); - auto at_compressed = CompressWithGzip(at_data.get(), DEFAULT_MAX_INPUT_SIZE); - auto over_compressed = - CompressWithGzip(over_data.get(), OVER_LIMIT_DATA_SIZE); - - // Test 1: 63 MB data with 64 MB limit - should succeed - { - auto decompressed = evbuffer_new(); - auto err = ni::DataCompressor::DecompressData( - ni::DataCompressor::Type::GZIP, under_compressed, decompressed, - DEFAULT_MAX_INPUT_SIZE); - ASSERT_TRUE((err == nullptr)) - << "63 MB data should decompress within 64 MB limit: " - << TRITONSERVER_ErrorMessage(err); - ASSERT_EQ(evbuffer_get_length(decompressed), UNDER_LIMIT_DATA_SIZE); - evbuffer_free(decompressed); - } - - // Test 2: 64 MB data with 64 MB limit - should succeed (exact boundary) - { - auto decompressed = evbuffer_new(); - auto err = ni::DataCompressor::DecompressData( - ni::DataCompressor::Type::GZIP, at_compressed, decompressed, - DEFAULT_MAX_INPUT_SIZE); - ASSERT_TRUE((err == nullptr)) - << "64 MB data should decompress at exact 64 MB limit: " - << TRITONSERVER_ErrorMessage(err); - ASSERT_EQ(evbuffer_get_length(decompressed), DEFAULT_MAX_INPUT_SIZE); - evbuffer_free(decompressed); - } - - // Test 3: 65 MB data with 64 MB limit - should fail - { - auto decompressed = evbuffer_new(); - auto err = ni::DataCompressor::DecompressData( - ni::DataCompressor::Type::GZIP, over_compressed, decompressed, - DEFAULT_MAX_INPUT_SIZE); - ASSERT_TRUE((err != nullptr)) << "65 MB data should fail with 64 MB limit"; - ASSERT_EQ(TRITONSERVER_ErrorCode(err), TRITONSERVER_ERROR_INVALID_ARG); - std::string error_msg = TRITONSERVER_ErrorMessage(err); - ASSERT_TRUE( - error_msg.find("exceeds the maximum allowed") != std::string::npos) - << "Error message should mention size limit: " << error_msg; - evbuffer_free(decompressed); - } - - // Test 4: 65 MB data with no limit - should succeed - { - auto decompressed = evbuffer_new(); - auto err = ni::DataCompressor::DecompressData( - ni::DataCompressor::Type::GZIP, over_compressed, decompressed, 0); - ASSERT_TRUE((err == nullptr)) - << "65 MB data should decompress with no limit: " - << TRITONSERVER_ErrorMessage(err); - ASSERT_EQ(evbuffer_get_length(decompressed), OVER_LIMIT_DATA_SIZE); - evbuffer_free(decompressed); - } - - evbuffer_free(under_compressed); - evbuffer_free(at_compressed); - evbuffer_free(over_compressed); -} - -// This test verifies that the max_decompressed_size parameter correctly -// limits the memory allocation during DEFLATE decompression. -TEST_F(DataCompressorTest, DecompressionSizeLimitDeflate) -{ - // Create test data buffers of different sizes - std::unique_ptr under_data(new char[UNDER_LIMIT_DATA_SIZE]); - std::unique_ptr at_data(new char[DEFAULT_MAX_INPUT_SIZE]); - std::unique_ptr over_data(new char[OVER_LIMIT_DATA_SIZE]); - memset(under_data.get(), 'A', UNDER_LIMIT_DATA_SIZE); - memset(at_data.get(), 'B', DEFAULT_MAX_INPUT_SIZE); - memset(over_data.get(), 'C', OVER_LIMIT_DATA_SIZE); - - // Compress each data set - auto under_compressed = - CompressWithDeflate(under_data.get(), UNDER_LIMIT_DATA_SIZE); - auto at_compressed = - CompressWithDeflate(at_data.get(), DEFAULT_MAX_INPUT_SIZE); - auto over_compressed = - CompressWithDeflate(over_data.get(), OVER_LIMIT_DATA_SIZE); - - // Test 1: 63 MB data with 64 MB limit - should succeed - { - auto decompressed = evbuffer_new(); - auto err = ni::DataCompressor::DecompressData( - ni::DataCompressor::Type::DEFLATE, under_compressed, decompressed, - DEFAULT_MAX_INPUT_SIZE); - ASSERT_TRUE((err == nullptr)) - << "63 MB data should decompress within 64 MB limit: " - << TRITONSERVER_ErrorMessage(err); - ASSERT_EQ(evbuffer_get_length(decompressed), UNDER_LIMIT_DATA_SIZE); - evbuffer_free(decompressed); - } - - // Test 2: 64 MB data with 64 MB limit - should succeed (exact boundary) - { - auto decompressed = evbuffer_new(); - auto err = ni::DataCompressor::DecompressData( - ni::DataCompressor::Type::DEFLATE, at_compressed, decompressed, - DEFAULT_MAX_INPUT_SIZE); - ASSERT_TRUE((err == nullptr)) - << "64 MB data should decompress at exact 64 MB limit: " - << TRITONSERVER_ErrorMessage(err); - ASSERT_EQ(evbuffer_get_length(decompressed), DEFAULT_MAX_INPUT_SIZE); - evbuffer_free(decompressed); - } - - // Test 3: 65 MB data with 64 MB limit - should fail - { - auto decompressed = evbuffer_new(); - auto err = ni::DataCompressor::DecompressData( - ni::DataCompressor::Type::DEFLATE, over_compressed, decompressed, - DEFAULT_MAX_INPUT_SIZE); - ASSERT_TRUE((err != nullptr)) << "65 MB data should fail with 64 MB limit"; - ASSERT_EQ(TRITONSERVER_ErrorCode(err), TRITONSERVER_ERROR_INVALID_ARG); - std::string error_msg = TRITONSERVER_ErrorMessage(err); - ASSERT_TRUE( - error_msg.find("exceeds the maximum allowed") != std::string::npos) - << "Error message should mention size limit: " << error_msg; - evbuffer_free(decompressed); - } - - // Test 4: 65 MB data with no limit - should succeed - { - auto decompressed = evbuffer_new(); - auto err = ni::DataCompressor::DecompressData( - ni::DataCompressor::Type::DEFLATE, over_compressed, decompressed, 0); - ASSERT_TRUE((err == nullptr)) - << "65 MB data should decompress with no limit: " - << TRITONSERVER_ErrorMessage(err); - ASSERT_EQ(evbuffer_get_length(decompressed), OVER_LIMIT_DATA_SIZE); - evbuffer_free(decompressed); - } - - evbuffer_free(under_compressed); - evbuffer_free(at_compressed); - evbuffer_free(over_compressed); -} - } // namespace int diff --git a/src/test/distributed_addsub/CMakeLists.txt b/src/test/distributed_addsub/CMakeLists.txt index 597e70308c..10a7148f8a 100644 --- a/src/test/distributed_addsub/CMakeLists.txt +++ b/src/test/distributed_addsub/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -24,7 +24,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -cmake_minimum_required(VERSION 3.31.8) +cmake_minimum_required(VERSION 3.17) project(tritondistributedaddsubbackend LANGUAGES C CXX) diff --git a/src/test/dyna_sequence/CMakeLists.txt b/src/test/dyna_sequence/CMakeLists.txt index 2bed7c2d36..1d0c095b9d 100644 --- a/src/test/dyna_sequence/CMakeLists.txt +++ b/src/test/dyna_sequence/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -24,7 +24,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -cmake_minimum_required(VERSION 3.31.8) +cmake_minimum_required(VERSION 3.17) project(tritondynasequencebackend LANGUAGES C CXX) diff --git a/src/test/implicit_state/CMakeLists.txt b/src/test/implicit_state/CMakeLists.txt index 4b6024a9c7..c868c5f1b7 100644 --- a/src/test/implicit_state/CMakeLists.txt +++ b/src/test/implicit_state/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -24,7 +24,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -cmake_minimum_required(VERSION 3.31.8) +cmake_minimum_required(VERSION 3.17) project(tritonimplicitsequencebackend LANGUAGES C CXX) diff --git a/src/test/iterative_sequence/CMakeLists.txt b/src/test/iterative_sequence/CMakeLists.txt index 4a44925524..9321e32049 100644 --- a/src/test/iterative_sequence/CMakeLists.txt +++ b/src/test/iterative_sequence/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -24,7 +24,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -cmake_minimum_required(VERSION 3.31.8) +cmake_minimum_required(VERSION 3.17) project(tritoniterativesequencebackend LANGUAGES C CXX) diff --git a/src/test/query_backend/CMakeLists.txt b/src/test/query_backend/CMakeLists.txt index 30c33a6b50..809499cd4b 100644 --- a/src/test/query_backend/CMakeLists.txt +++ b/src/test/query_backend/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -24,7 +24,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -cmake_minimum_required(VERSION 3.31.8) +cmake_minimum_required(VERSION 3.17) project(tritonquerybackend LANGUAGES C CXX) diff --git a/src/test/repoagent/relocation_repoagent/CMakeLists.txt b/src/test/repoagent/relocation_repoagent/CMakeLists.txt index d0b97b5d24..8c2e27be36 100644 --- a/src/test/repoagent/relocation_repoagent/CMakeLists.txt +++ b/src/test/repoagent/relocation_repoagent/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -24,7 +24,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -cmake_minimum_required(VERSION 3.31.8) +cmake_minimum_required(VERSION 3.17) project(tritonrelocationrepoagent LANGUAGES C CXX) diff --git a/src/test/sequence/CMakeLists.txt b/src/test/sequence/CMakeLists.txt index ab105f20eb..5cb8b65362 100644 --- a/src/test/sequence/CMakeLists.txt +++ b/src/test/sequence/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2021-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -24,7 +24,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -cmake_minimum_required(VERSION 3.31.8) +cmake_minimum_required(VERSION 3.17) project(tritonsequencebackend LANGUAGES C CXX) diff --git a/src/test/tensor_size_test.cc b/src/test/tensor_size_test.cc deleted file mode 100644 index 26969fcd42..0000000000 --- a/src/test/tensor_size_test.cc +++ /dev/null @@ -1,491 +0,0 @@ -// Copyright 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions -// are met: -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in the -// documentation and/or other materials provided with the distribution. -// * Neither the name of NVIDIA CORPORATION nor the names of its -// contributors may be used to endorse or promote products derived -// from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -// OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -#include "gtest/gtest.h" - -// Undefine the FAIL() macro inside Triton code to avoid redefine error -// from gtest. Okay as FAIL() is not used in data_compressor -#ifdef FAIL -#undef FAIL -#endif - -#include -#include - -#include "model_config_utils.h" -#undef RETURN_IF_ERROR // core (Status) vs backend (TRITONSERVER_Error*); avoid - // redefinition -#include "triton/backend/backend_common.h" -#include "triton/common/model_config.h" - -namespace tb = triton::backend; -namespace tc = triton::common; -namespace tcore = triton::core; - -// Core's linker script hides C++ symbols from libtritonserver.so. -// Provide the small set of definitions the header-only templates need. -const tcore::Status tcore::Status::Success(tcore::Status::Code::SUCCESS); - -inference::DataType -tcore::TritonToDataType(const TRITONSERVER_DataType dtype) -{ - return static_cast(dtype); -} - -namespace { - -struct TritonServerError { - TritonServerError(TRITONSERVER_Error_Code code, const char* msg) - : code_(code), msg_(msg) - { - } - TRITONSERVER_Error_Code code_; - std::string msg_; -}; - -} // namespace - -#ifdef __cplusplus -extern "C" { -#endif - -TRITONSERVER_Error_Code -TRITONSERVER_ErrorCode(TRITONSERVER_Error* error) -{ - return (reinterpret_cast(error))->code_; -} - -const char* -TRITONSERVER_ErrorMessage(TRITONSERVER_Error* error) -{ - return (reinterpret_cast(error))->msg_.c_str(); -} - -#ifdef __cplusplus -} -#endif - -namespace { - -enum class ErrorCode { - kInvalidDim = tc::INVALID_SIZE, - kOverflow = tc::OVERFLOW_SIZE, -}; - -static const std::string kTensorName{"input0"}; - -void -assert_get_element_count_success( - std::vector& shape, int64_t expected_cnt) -{ - int64_t cnt; - TRITONSERVER_Error* err; - - // Backend (old APIs) - ASSERT_EQ(expected_cnt, tb::GetElementCount(shape.data(), shape.size())); - ASSERT_EQ(expected_cnt, tb::GetElementCount(shape)); - - // Backend (new APIs) - err = tb::GetElementCount(shape.data(), shape.size(), &cnt); - ASSERT_EQ(err, nullptr); - ASSERT_EQ(cnt, expected_cnt); - err = tb::GetElementCount(shape, &cnt); - ASSERT_EQ(err, nullptr); - ASSERT_EQ(cnt, expected_cnt); - - // Common - ASSERT_EQ(tc::GetElementCount(shape), expected_cnt); - - // Core - cnt = 0; - auto status = tcore::GetElementCount(shape, kTensorName, &cnt); - ASSERT_TRUE(status.IsOk()) << status.Message(); - ASSERT_EQ(cnt, expected_cnt); -} - -void -assert_get_element_count_error( - std::vector& shape, ErrorCode error_code, - const std::string& error_msg) -{ - int64_t cnt; - TRITONSERVER_Error* err; - - // Backend (old APIs) - ASSERT_EQ( - static_cast(error_code), - tb::GetElementCount(shape.data(), shape.size())); - ASSERT_EQ(static_cast(error_code), tb::GetElementCount(shape)); - - // Backend (new APIs) - err = tb::GetElementCount(shape.data(), shape.size(), &cnt); - ASSERT_NE(err, nullptr); - ASSERT_EQ(TRITONSERVER_ERROR_INVALID_ARG, TRITONSERVER_ErrorCode(err)); - ASSERT_STREQ(error_msg.c_str(), TRITONSERVER_ErrorMessage(err)); - err = tb::GetElementCount(shape, &cnt); - ASSERT_NE(err, nullptr); - ASSERT_EQ(TRITONSERVER_ERROR_INVALID_ARG, TRITONSERVER_ErrorCode(err)); - ASSERT_STREQ(error_msg.c_str(), TRITONSERVER_ErrorMessage(err)); - - // Common - ASSERT_EQ(tc::GetElementCount(shape), static_cast(error_code)); - - // Core - cnt = 0; - auto status = tcore::GetElementCount(shape, kTensorName, &cnt); - ASSERT_FALSE(status.IsOk()); - ASSERT_EQ(status.StatusCode(), triton::core::Status::Code::INVALID_ARG); - ASSERT_TRUE( - std::string(status.Message()) - .find( - error_code == ErrorCode::kInvalidDim - ? "invalid dimension" - : "exceeds maximum size") != std::string::npos); -} - -void -assert_get_byte_size_success( - TRITONSERVER_DataType dtype, std::vector& shape, - int64_t expected_size, bool test_core = true) -{ - int64_t size; - TRITONSERVER_Error* err; - - // Backend (old API) - ASSERT_EQ(expected_size, tb::GetByteSize(dtype, shape)); - - // Backend (new API) - err = tb::GetByteSize(dtype, shape, &size); - ASSERT_EQ(err, nullptr); - ASSERT_EQ(expected_size, size); - - // Common - inference::DataType core_dtype = tcore::TritonToDataType(dtype); - ASSERT_EQ(tc::GetByteSize(core_dtype, shape), expected_size); - - // Core - if (test_core) { - size = 0; - auto status = tcore::GetByteSize(core_dtype, shape, kTensorName, &size); - ASSERT_TRUE(status.IsOk()) << status.Message(); - ASSERT_EQ(size, expected_size); - } -} - -void -assert_get_byte_size_error( - TRITONSERVER_DataType dtype, std::vector& shape, - ErrorCode error_code, const std::string& error_msg) -{ - int64_t size; - TRITONSERVER_Error* err; - - // Backend (old API) - ASSERT_EQ(static_cast(error_code), tb::GetByteSize(dtype, shape)); - - // Backend (new API) - err = tb::GetByteSize(dtype, shape, &size); - ASSERT_NE(err, nullptr); - ASSERT_EQ(TRITONSERVER_ERROR_INVALID_ARG, TRITONSERVER_ErrorCode(err)); - ASSERT_EQ(error_msg, TRITONSERVER_ErrorMessage(err)); - - // Common - inference::DataType core_dtype = tcore::TritonToDataType(dtype); - ASSERT_EQ( - tc::GetByteSize(core_dtype, shape), error_code == ErrorCode::kInvalidDim - ? tc::INVALID_SIZE - : tc::OVERFLOW_SIZE); - - // Core - size = 0; - auto status = tcore::GetByteSize(core_dtype, shape, kTensorName, &size); - ASSERT_FALSE(status.IsOk()); - ASSERT_EQ(status.StatusCode(), triton::core::Status::Code::INVALID_ARG); - ASSERT_TRUE( - std::string(status.Message()) - .find( - error_code == ErrorCode::kInvalidDim - ? "invalid dimension" - : "exceeds maximum size") != std::string::npos); -} - -class GetElementCountTest : public ::testing::Test { - public: - GetElementCountTest() {} -}; - -TEST_F(GetElementCountTest, GetElementCount) -{ - std::vector shape; - int64_t expected_cnt; - - // Test 1: empty shape - shape = {}; - expected_cnt = 0; - assert_get_element_count_success(shape, expected_cnt); - - // Test 2: single dim - shape = {8}; - expected_cnt = 8; - assert_get_element_count_success(shape, expected_cnt); - - // Test 3: multiple dims - shape = {1, 2, 3, 4}; - expected_cnt = 24; - assert_get_element_count_success(shape, expected_cnt); -} - -TEST_F(GetElementCountTest, GetElementCountWildcard) -{ - std::vector shape; - int64_t expected_cnt = tc::WILDCARD_SIZE; - - // Test 1: -1 dim - shape = {-1}; - assert_get_element_count_success(shape, expected_cnt); - - // Test 2: one -1 dim - shape = {-1, 8, 8}; - assert_get_element_count_success(shape, expected_cnt); - - // Test 3: multiple -1 dims - shape = {8, -1, -1}; - assert_get_element_count_success(shape, expected_cnt); - - // Test 4: -1 dim before overflow - shape = {-1, 1LL << 32, 1LL << 31}; - assert_get_element_count_success(shape, expected_cnt); -} - -TEST_F(GetElementCountTest, GetElementCountZero) -{ - std::vector shape; - int64_t expected_cnt = 0; - - // Test 1: 0 dim - shape = {0}; - assert_get_element_count_success(shape, expected_cnt); - - // Test 2: one 0 dim - shape = {1, 8, 0}; - assert_get_element_count_success(shape, expected_cnt); - - shape = {0, 1, 8}; - assert_get_element_count_success(shape, expected_cnt); - - // Test 3: multiple 0 dims - shape = {8, 0, 0}; - assert_get_element_count_success(shape, expected_cnt); -} - -TEST_F(GetElementCountTest, GetElementCountInvalidDim) -{ - std::vector shape; - std::string error_msg; - - // Test 1: single invalid dim - shape = {1, -2}; - error_msg = std::string("shape") + tb::ShapeToString(shape) + - " contains an invalid dim."; - assert_get_element_count_error(shape, ErrorCode::kInvalidDim, error_msg); - - // Test 2: multiple invalid dims - shape = {1, -2, -3}; - error_msg = std::string("shape") + tb::ShapeToString(shape) + - " contains an invalid dim."; - assert_get_element_count_error(shape, ErrorCode::kInvalidDim, error_msg); - - // Test 3: valid but overflow dim - shape = {1, 1LL << 63}; - error_msg = std::string("shape") + tb::ShapeToString(shape) + - " contains an invalid dim."; - assert_get_element_count_error(shape, ErrorCode::kInvalidDim, error_msg); -} - -TEST_F(GetElementCountTest, GetElementCountOverflow) -{ - std::vector shape; - std::string error_msg; - - // Test 1: no overflow - shape = {1LL << 31, 1LL << 31}; - int64_t expected_cnt = 1LL << 62; - assert_get_element_count_success(shape, expected_cnt); - - // Test 2: overflows - shape = {1LL << 32, 1LL << 31}; - error_msg = "unexpected integer overflow while calculating element count."; - assert_get_element_count_error(shape, ErrorCode::kOverflow, error_msg); - - // Test 3: overflows before -1 dim - shape = {1LL << 32, 1LL << 31, -1}; - error_msg = "unexpected integer overflow while calculating element count."; - assert_get_element_count_error(shape, ErrorCode::kOverflow, error_msg); -} - -class GetByteSizeTest : public ::testing::Test { - public: - GetByteSizeTest() {} -}; - -TEST_F(GetByteSizeTest, GetByteSize) -{ - TRITONSERVER_DataType dtype = TRITONSERVER_TYPE_INT32; - std::vector shape; - int64_t expected_size; - - // Test 1: empty shape - shape = {}; - expected_size = 0; - assert_get_byte_size_success(dtype, shape, expected_size); - - // Test 2: single dim - shape = {8}; - expected_size = 8 * TRITONSERVER_DataTypeByteSize(dtype); - assert_get_byte_size_success(dtype, shape, expected_size); - - // Test 3: multiple dims - shape = {1, 2, 3, 4}; - expected_size = 24 * TRITONSERVER_DataTypeByteSize(dtype); - assert_get_byte_size_success(dtype, shape, expected_size); - - // Test 4: multiple dims with 0 - shape = {0, 1, 8}; - expected_size = 0; - assert_get_byte_size_success(dtype, shape, expected_size); -} - -TEST_F(GetByteSizeTest, GetByteSizeWildcard) -{ - TRITONSERVER_DataType dtype; - std::vector shape; - int64_t expected_size = tc::WILDCARD_SIZE; - - // Test 1: invalid dtype - dtype = TRITONSERVER_TYPE_INVALID; - shape = {8, 8}; - assert_get_byte_size_success(dtype, shape, expected_size); - - // Test 2: bytes dtype - dtype = TRITONSERVER_TYPE_BYTES; - shape = {8, 8}; - assert_get_byte_size_success(dtype, shape, expected_size, false); - // test core explicitly as it treats string dtype size as 4 - int64_t size = 0; - inference::DataType core_dtype = tcore::TritonToDataType(dtype); - auto status = tcore::GetByteSize(core_dtype, shape, kTensorName, &size); - ASSERT_TRUE(status.IsOk()) << status.Message(); - ASSERT_EQ(size, sizeof(int32_t) * 8 * 8); - - - // Test 3: invalid shape and element count overflows - dtype = TRITONSERVER_TYPE_INVALID; - shape = {1LL << 40, 1LL << 40}; - assert_get_byte_size_success(dtype, shape, expected_size); - - // Test 4: negative shape - dtype = TRITONSERVER_TYPE_INT32; - shape = {-1, 8}; - assert_get_byte_size_success(dtype, shape, expected_size); -} - -TEST_F(GetByteSizeTest, GetByteSizeZero) -{ - TRITONSERVER_DataType dtype = TRITONSERVER_TYPE_INT32; - std::vector shape; - int64_t expected_cnt = 0; - - // Test 1: 0 dim - shape = {0}; - assert_get_byte_size_success(dtype, shape, expected_cnt); - - // Test 2: one 0 dim - shape = {1, 8, 0}; - assert_get_byte_size_success(dtype, shape, expected_cnt); - - shape = {0, 1, 8}; - assert_get_byte_size_success(dtype, shape, expected_cnt); - - // Test 3: multiple 0 dims - shape = {8, 0, 0}; - assert_get_byte_size_success(dtype, shape, expected_cnt); -} - -TEST_F(GetByteSizeTest, GetByteSizeInvalidDim) -{ - TRITONSERVER_DataType dtype = TRITONSERVER_TYPE_INT32; - std::vector shape; - std::string error_msg; - - // Test 1: single invalid dim - shape = {1, -2}; - error_msg = std::string("shape") + tb::ShapeToString(shape) + - " contains an invalid dim."; - assert_get_byte_size_error(dtype, shape, ErrorCode::kInvalidDim, error_msg); - - // Test 2: multiple invalid dims - shape = {1, -2, -3}; - error_msg = std::string("shape") + tb::ShapeToString(shape) + - " contains an invalid dim."; - assert_get_byte_size_error(dtype, shape, ErrorCode::kInvalidDim, error_msg); - - // Test 3: valid but overflow dim - shape = {1, 1LL << 63}; - error_msg = std::string("shape") + tb::ShapeToString(shape) + - " contains an invalid dim."; - assert_get_byte_size_error(dtype, shape, ErrorCode::kInvalidDim, error_msg); -} - -TEST_F(GetByteSizeTest, GetByteSizeOverflow) -{ - TRITONSERVER_DataType dtype = TRITONSERVER_TYPE_INT32; - std::vector shape; - std::string error_msg; - - // Test 1: no overflow - shape = {1LL << 30, 1LL << 30}; - int64_t expected_size = (1LL << 60) * TRITONSERVER_DataTypeByteSize(dtype); - assert_get_byte_size_success(dtype, shape, expected_size); - - // Test 2: element count overflows - shape = {1LL << 32, 1LL << 31}; - error_msg = "unexpected integer overflow while calculating byte size."; - assert_get_byte_size_error(dtype, shape, ErrorCode::kOverflow, error_msg); - - // Test 3: valid element count but byte size overflows - shape = {1LL << 31, 1LL << 30}; - error_msg = "unexpected integer overflow while calculating byte size."; - assert_get_byte_size_error(dtype, shape, ErrorCode::kOverflow, error_msg); -} - -} // namespace - -int -main(int argc, char** argv) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/src/tracer.cc b/src/tracer.cc index 7955cfe3bc..5557106dfd 100644 --- a/src/tracer.cc +++ b/src/tracer.cc @@ -1,4 +1,4 @@ -// Copyright 2019-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -740,27 +740,16 @@ TraceManager::TraceRelease(TRITONSERVER_InferenceTrace* trace, void* userp) LOG_TRITONSERVER_ERROR( TRITONSERVER_InferenceTraceId(trace, &id), "getting trace id"); - auto ts_ptr = reinterpret_cast*>(userp); - std::shared_ptr tracer_sp; - bool delete_ts = false; - { - std::lock_guard lk((*ts_ptr)->mtx_); - (*ts_ptr)->spawned_traces_tracker_.erase(id); - // The userp will be shared with the trace children, so only delete it - // if no more TraceRelease calls are expected - if ((*ts_ptr)->spawned_traces_tracker_.empty()) { - // Move the trace shared_ptr out inside lock to ensure mutex stays alive - // and destruct outside lock - tracer_sp = std::move(*ts_ptr); - delete_ts = true; - delete ts_ptr; - } - LOG_TRITONSERVER_ERROR( - TRITONSERVER_InferenceTraceDelete(trace), "deleting trace"); - } - if (delete_ts) { - tracer_sp.reset(); + auto ts = reinterpret_cast*>(userp); + std::lock_guard lk((*ts)->mtx_); + (*ts)->spawned_traces_tracker_.erase(id); + // The userp will be shared with the trace children, so only delete it + // if no more TraceRelease calls are expected + if ((*ts)->spawned_traces_tracker_.empty()) { + delete ts; } + LOG_TRITONSERVER_ERROR( + TRITONSERVER_InferenceTraceDelete(trace), "deleting trace"); } const char* diff --git a/src/triton_signal.cc b/src/triton_signal.cc index 05fcb97444..1a02144d2a 100644 --- a/src/triton_signal.cc +++ b/src/triton_signal.cc @@ -1,4 +1,4 @@ -// Copyright 2020-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2020-2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -137,9 +137,6 @@ RegisterSignalHandler() signal(SIGSEGV, ErrorSignalHandler); signal(SIGABRT, ErrorSignalHandler); - // Ignore SIGPIPE to prevent crash on AWS SDK writing to closed idle sockets - signal(SIGPIPE, SIG_IGN); - return nullptr; // success } diff --git a/src/vertex_ai_server.cc b/src/vertex_ai_server.cc index 2e59a84e45..a7792d9c1b 100644 --- a/src/vertex_ai_server.cc +++ b/src/vertex_ai_server.cc @@ -1,4 +1,4 @@ -// Copyright 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2021-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -42,12 +42,10 @@ VertexAiAPIServer::VertexAiAPIServer( const std::shared_ptr& shm_manager, const int32_t port, const std::string address, const int thread_cnt, const std::string& prediction_route, const std::string& health_route, - const std::string& default_model_name, const size_t max_input_size, - const RestrictedFeatures& restricted_apis) + const std::string& default_model_name) : HTTPAPIServer( server, trace_manager, shm_manager, port, false /* reuse_port */, - address, "" /* header_forward_pattern */, thread_cnt, max_input_size, - restricted_apis), + address, "" /* header_forward_pattern */, thread_cnt), prediction_regex_(prediction_route), health_regex_(health_route), health_mode_("ready"), model_name_(default_model_name), model_version_str_("") @@ -79,7 +77,7 @@ VertexAiAPIServer::GetInferenceHeaderLength( int32_t parsed_value; try { parsed_value = - std::stoi(content_type_c_str + binary_mime_type_.length()); + std::atoi(content_type_c_str + binary_mime_type_.length()); } catch (const std::invalid_argument& ia) { return TRITONSERVER_ErrorNew( @@ -196,40 +194,32 @@ VertexAiAPIServer::Handle(evhtp_request_t* req) } else if (RE2::FullMatch( redirect_endpoint, systemsharedmemory_regex_, ®ion, &action)) { + // system shared memory if (action == "status") { req->method = htp_method_GET; - HandleSystemSharedMemory(req, region, action); - return; } - // Only read-only status queries are permitted through redirect. - // Mutating operations (register/unregister) require the core - // HTTP endpoint. - LOG_VERBOSE(1) << "Vertex AI redirect blocked: " << redirect_endpoint; - evhtp_send_reply(req, EVHTP_RES_FORBIDDEN); + HandleSystemSharedMemory(req, region, action); return; } else if (RE2::FullMatch( redirect_endpoint, cudasharedmemory_regex_, ®ion, &action)) { + // cuda shared memory if (action == "status") { req->method = htp_method_GET; - HandleCudaSharedMemory(req, region, action); - return; } - LOG_VERBOSE(1) << "Vertex AI redirect blocked: " << redirect_endpoint; - evhtp_send_reply(req, EVHTP_RES_FORBIDDEN); + HandleCudaSharedMemory(req, region, action); return; } else if (RE2::FullMatch( redirect_endpoint, modelcontrol_regex_, &repo_name, &kind, &model_name, &action)) { + // model repository if (kind == "index") { HandleRepositoryIndex(req, repo_name); return; + } else if (kind.find("models", 0) == 0) { + HandleRepositoryControl(req, repo_name, model_name, action); + return; } - // Only repository index queries are permitted through redirect. - // Model load/unload requires the core HTTP endpoint. - LOG_VERBOSE(1) << "Vertex AI redirect blocked: " << redirect_endpoint; - evhtp_send_reply(req, EVHTP_RES_FORBIDDEN); - return; } } } @@ -279,7 +269,6 @@ VertexAiAPIServer::Create( triton::server::TraceManager* trace_manager, const std::shared_ptr& shm_manager, const int32_t port, const std::string address, const int thread_cnt, - const size_t max_input_size, const RestrictedFeatures& restricted_apis, std::string default_model_name, std::unique_ptr* http_server) { auto predict_route = GetEnvironmentVariableOrDefault("AIP_PREDICT_ROUTE", ""); @@ -355,8 +344,7 @@ VertexAiAPIServer::Create( http_server->reset(new VertexAiAPIServer( server, trace_manager, shm_manager, port, address, thread_cnt, - predict_route, health_route, default_model_name, max_input_size, - restricted_apis)); + predict_route, health_route, default_model_name)); const std::string addr = address + ":" + std::to_string(port); LOG_INFO << "Started Vertex AI HTTPService at " << addr; diff --git a/src/vertex_ai_server.h b/src/vertex_ai_server.h index 3221c5ccd6..13fb62675a 100644 --- a/src/vertex_ai_server.h +++ b/src/vertex_ai_server.h @@ -1,4 +1,4 @@ -// Copyright 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2021-2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions @@ -37,7 +37,6 @@ class VertexAiAPIServer : public HTTPAPIServer { triton::server::TraceManager* trace_manager, const std::shared_ptr& smb_manager, const int32_t port, const std::string address, const int thread_cnt, - const size_t max_input_size, const RestrictedFeatures& restricted_apis, std::string default_model_name, std::unique_ptr* vertex_ai_server); @@ -48,8 +47,7 @@ class VertexAiAPIServer : public HTTPAPIServer { const std::shared_ptr& shm_manager, const int32_t port, const std::string address, const int thread_cnt, const std::string& prediction_route, const std::string& health_route, - const std::string& default_model_name, const size_t max_input_size, - const RestrictedFeatures& restricted_apis); + const std::string& default_model_name); void Handle(evhtp_request_t* req) override; diff --git a/tools/add_copyright.py b/tools/add_copyright.py index 51ae8e5d2a..a51ffbfc64 100644 --- a/tools/add_copyright.py +++ b/tools/add_copyright.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -77,10 +77,12 @@ def update_and_get_license() -> str: """ Updates the copyright year in the LICENSE file if necessary and then returns its contents. - - Note: LICENSE file maintains a year range if it has an older starting year. """ - update_copyright_year(LICENSE_PATH) + # TODO: Check if this is right - if the license file needs to have a range, + # we need to remove the range before returning the license text. + # + # License file should always have the current year. + update_copyright_year(LICENSE_PATH, disallow_range=True) with open(LICENSE_PATH, "r") as license_file: return license_file.read() @@ -251,11 +253,6 @@ def rst(path): def add_copyrights(paths): for path in paths: - # Special case: LICENSE file only needs year update - if os.path.basename(path) == "LICENSE": - update_copyright_year(path) - continue - for match, handler in FILE_TYPE_HANDLERS.items(): if match(path): handler(path) @@ -373,6 +370,5 @@ def test_adding_new_copyrights(content, extension, expected): assert process_text(content, extension).startswith(expected) -def test_license_has_current_year(): - # LICENSE file should have the current year (either as single year or end of range) - assert f"{current_year}, NVIDIA CORPORATION" in LICENSE_TEXT +def test_license_has_no_range(): + assert LICENSE_TEXT.startswith(f"Copyright (c) {current_year},") From 797ec6807eba577ff55a3d83c3a10eec24391493 Mon Sep 17 00:00:00 2001 From: Madhukar K Date: Tue, 9 Jun 2026 13:15:06 -0700 Subject: [PATCH 228/230] Added DB logic and new Dockerfile --- .gitignore | 1 + CMakeLists.txt | 5 + Dockerfile | 93 ++++ config/database_config.sample.json | 13 + src/CMakeLists.txt | 20 + src/database_config.cc | 204 +++++++++ src/database_config.h | 56 +++ src/main.cc | 126 ++++++ src/mysql_odbc_connection_pool.cc | 697 +++++++++++++++++++++++++++++ src/mysql_odbc_connection_pool.h | 163 +++++++ 10 files changed, 1378 insertions(+) create mode 100644 Dockerfile create mode 100644 config/database_config.sample.json create mode 100644 src/database_config.cc create mode 100644 src/database_config.h create mode 100644 src/mysql_odbc_connection_pool.cc create mode 100644 src/mysql_odbc_connection_pool.h diff --git a/.gitignore b/.gitignore index 31f5d9f5f4..01abc6eaab 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ qa/L0_openai/openai tensorrtllm_models tensorrtllm_mistral_models/ custom_tokenizer +replace-artifacts/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 211e7578ac..de1e229c2e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,6 +80,10 @@ option(TRITON_ENABLE_GCS "Include GCS Filesystem support in server" OFF) option(TRITON_ENABLE_S3 "Include S3 Filesystem support in server" OFF) option(TRITON_ENABLE_AZURE_STORAGE "Include Azure Storage Filesystem support in server" OFF) +option(TRITON_ENABLE_MYSQL_ODBC + "Enable MySQL ODBC connection pool in tritonserver (requires unixODBC / ODBC dev package)" + OFF) + # Need to know if TensorRT is available when building unit tests option(TRITON_ENABLE_TENSORRT "Include TensorRT backend in server" OFF) @@ -276,6 +280,7 @@ ExternalProject_Add(triton-server -DTRITON_ENABLE_S3:BOOL=${TRITON_ENABLE_S3} -DTRITON_ENABLE_TENSORRT:BOOL=${TRITON_ENABLE_TENSORRT} -DTRITON_ENABLE_ENSEMBLE:BOOL=${TRITON_ENABLE_ENSEMBLE} + -DTRITON_ENABLE_MYSQL_ODBC:BOOL=${TRITON_ENABLE_MYSQL_ODBC} -DTRITON_MIN_CXX_STANDARD:STRING=${TRITON_MIN_CXX_STANDARD} -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} -DCMAKE_INSTALL_PREFIX:PATH=${TRITON_INSTALL_PREFIX} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..f9cf801b81 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,93 @@ +# Copyright 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Derivative image: start from an official Triton server image and replace only +# the tritonserver executable and libtritonserver.so. Backends, Python wheels, +# and the rest of the filesystem stay unchanged from the base image. +# +# After a local CMake install (same layout as your -DCMAKE_INSTALL_PREFIX): +# install/bin/tritonserver +# install/lib/libtritonserver.so +# +# Prepare artifacts (from repository root): +# mkdir -p replace-artifacts +# cp install/bin/tritonserver replace-artifacts/ +# cp install/lib/libtritonserver.so replace-artifacts/ +# cp /etc/odbc.ini replace-artifacts/odbc.ini # optional if databaseIp is set (DSN unused) +# Run `odbcinst -q -d` inside the built image to see the exact ODBC driver name for +# optional JSON field "odbcDriverName" if the default fails. +# +# Build (from repository root; match your Triton tag, e.g. r25.03): +# docker build \ +# --build-arg BASE_IMAGE=nvcr.io/nvidia/tritonserver:25.03-py3 \ +# -t tritonserver:25.03-custom . +# +# CPU-only base example: +# docker build \ +# --build-arg BASE_IMAGE=nvcr.io/nvidia/tritonserver:25.03-py3-min \ +# -t tritonserver:25.03-custom-cpu . +# +# If your base image stores libtritonserver.so under lib64: +# --build-arg TRITON_LIB_SUBDIR=lib64 +# +# Ubuntu 24.04 (noble) base images: use Connector package for 24.04, e.g.: +# --build-arg MYSQL_ODBC_DEB_VERSION=9.7.0-1ubuntu24.04 +# +# Run (mount a model repository and optionally override DM config): +# docker run --rm --gpus=all \ +# -p8000:8000 -p8001:8001 -p8002:8002 \ +# -v /path/to/model_repo:/models:ro \ +# -v /path/to/your-triton-dmconfig.json:/etc/triton-dmconfig.json:ro \ +# tritonserver:25.03-custom \ +# tritonserver --model-repository=/models +# +# For CPU-only, drop --gpus=all and use a CPU/min base image. + +ARG BASE_IMAGE=nvcr.io/nvidia/tritonserver:25.03-py3 + +FROM ${BASE_IMAGE} + +ARG TRITON_INSTALL_PREFIX=/opt/tritonserver +ARG TRITON_LIB_SUBDIR=lib + +ARG MYSQL_ODBC_DEB_VERSION=9.7.0-1ubuntu22.04 +ARG MYSQL_ODBC_DEB_ARCH=amd64 + +# unixODBC + official MySQL Connector/ODBC .deb (libmyodbc8 is often only in Ubuntu Universe +# or missing on minimal images). Override MYSQL_ODBC_DEB_* for noble/arm64, etc. +USER root + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + unixodbc \ + odbcinst; \ + DEB="mysql-connector-odbc_${MYSQL_ODBC_DEB_VERSION}_${MYSQL_ODBC_DEB_ARCH}.deb"; \ + curl -fsSL -o "/tmp/${DEB}" \ + "https://repo.mysql.com/apt/ubuntu/pool/mysql-tools/m/mysql-connector-odbc/${DEB}"; \ + apt-get install -y "/tmp/${DEB}" || apt-get -fy install; \ + rm -f "/tmp/${DEB}"; \ + rm -rf /var/lib/apt/lists/* + +# Optional DSN file (only used when databaseIp is empty in triton-dmconfig.json). +COPY replace-artifacts/odbc.ini /etc/odbc.ini +RUN chmod 644 /etc/odbc.ini + +# Default DM database metadata (override at runtime with -v ...:/etc/triton-dmconfig.json:ro) +COPY replace-artifacts/triton-dmconfig.json /etc/triton-dmconfig.json +RUN chmod 644 /etc/triton-dmconfig.json + +# Paths relative to the build context (repository root when building with ".") +COPY replace-artifacts/tritonserver ${TRITON_INSTALL_PREFIX}/bin/tritonserver +COPY replace-artifacts/libtritonserver.so \ + ${TRITON_INSTALL_PREFIX}/${TRITON_LIB_SUBDIR}/libtritonserver.so + +# Match ownership used by generated Triton Dockerfiles (triton-server uid) +RUN chown 1000:1000 \ + ${TRITON_INSTALL_PREFIX}/bin/tritonserver \ + ${TRITON_INSTALL_PREFIX}/${TRITON_LIB_SUBDIR}/libtritonserver.so \ + && chmod 755 \ + ${TRITON_INSTALL_PREFIX}/bin/tritonserver \ + ${TRITON_INSTALL_PREFIX}/${TRITON_LIB_SUBDIR}/libtritonserver.so + diff --git a/config/database_config.sample.json b/config/database_config.sample.json new file mode 100644 index 0000000000..db12e70189 --- /dev/null +++ b/config/database_config.sample.json @@ -0,0 +1,13 @@ +{ + "databaseIp" : "10.0.0.1", + "databasePort" : 3306, + "odbcDriverName" : "MySQL ODBC 9.7 Unicode Driver", + "primaryDSNName" : "PrimaryCentralisedMySQL", + "secondaryDSNName" : "SecondaryCentralisedMySQL", + "dsnUserName" : "kdbuser", + "dsnUserPassword" : "REPLACE_WITH_SECRET", + "queryRetryCount": 3, + "dcId": 1, + "minPoolConnections": 2, + "maxPoolConnections": 5 +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8824e2ed6a..6ac4d61915 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -90,6 +90,12 @@ endif() # find_package(re2 REQUIRED) +option(TRITON_ENABLE_MYSQL_ODBC "Build tritonserver with MySQL ODBC connection pool (requires ODBC::ODBC / unixODBC)" OFF) +if(TRITON_ENABLE_MYSQL_ODBC) + find_package(ODBC REQUIRED) + message(STATUS "Using ODBC ${ODBC_VERSION}") +endif() + # # tritonserver executable # @@ -101,8 +107,10 @@ add_executable( main.cc shared_memory_manager.cc triton_signal.cc + database_config.cc classification.h common.h + database_config.h shared_memory_manager.h triton_signal.h ) @@ -155,6 +163,17 @@ else() ) endif() +if(TRITON_ENABLE_MYSQL_ODBC) + target_sources( + main + PRIVATE + mysql_odbc_connection_pool.cc + mysql_odbc_connection_pool.h + ) + target_link_libraries(main PRIVATE ODBC::ODBC) + target_compile_definitions(main PRIVATE TRITON_ENABLE_MYSQL_ODBC=1) +endif() + set(LIB_DIR "lib") if(LINUX) file(STRINGS "/etc/os-release" DISTRO_ID_LIKE REGEX "ID_LIKE") @@ -180,6 +199,7 @@ target_link_libraries( triton-common-async-work-queue # from repo-common triton-common-error # from repo-common triton-common-logging # from repo-common + triton-common-json # from repo-common (RapidJSON) triton-core-serverapi # from repo-core triton-core-serverstub # from repo-core ) diff --git a/src/database_config.cc b/src/database_config.cc new file mode 100644 index 0000000000..cb312dec5b --- /dev/null +++ b/src/database_config.cc @@ -0,0 +1,204 @@ +// Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of NVIDIA CORPORATION nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +// OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "database_config.h" + +#include +#include + +#include +#include +#include +#include +#include + +namespace triton { namespace server { + +namespace { + +std::optional ReadEntireFile(const std::string& path, std::string* contents) +{ + std::ifstream in(path, std::ios::binary); + if (!in) { + return std::string("failed to open file: ") + path; + } + std::ostringstream ss; + ss << in.rdbuf(); + if (!in && !in.eof()) { + return std::string("failed to read file: ") + path; + } + *contents = ss.str(); + return std::nullopt; +} + +std::optional ExpectString(const rapidjson::Value& obj, const char* key, std::string* field, bool required) +{ + if (!obj.HasMember(key)) { + if (required) { + return std::string("missing required JSON field: ") + key; + } + field->clear(); + return std::nullopt; + } + const auto& v = obj[key]; + if (!v.IsString()) { + return std::string("JSON field '") + key + "' must be a string"; + } + *field = v.GetString(); + return std::nullopt; +} + +std::optional OptionalInt(const rapidjson::Value& obj, const char* key, int* out, int def) +{ + if (!obj.HasMember(key)) { + *out = def; + return std::nullopt; + } + const auto& v = obj[key]; + if (!v.IsInt()) { + return std::string("JSON field '") + key + "' must be an integer"; + } + *out = v.GetInt(); + return std::nullopt; +} + +std::optional OptionalIntFlexible(const rapidjson::Value& obj, const char* key, int* out, int def) +{ + if (!obj.HasMember(key)) { + *out = def; + return std::nullopt; + } + const auto& v = obj[key]; + int n = 0; + if (v.IsInt()) { + n = v.GetInt(); + } else if (v.IsUint()) { + if (v.GetUint() > static_cast(INT_MAX)) { + return std::string("JSON field '") + key + "' is out of range"; + } + n = static_cast(v.GetUint()); + } else if (v.IsInt64()) { + const int64_t v64 = v.GetInt64(); + if (v64 < INT_MIN || v64 > INT_MAX) { + return std::string("JSON field '") + key + "' is out of range"; + } + n = static_cast(v64); + } else if (v.IsUint64()) { + const uint64_t v64 = v.GetUint64(); + if (v64 > static_cast(INT_MAX)) { + return std::string("JSON field '") + key + "' is out of range"; + } + n = static_cast(v64); + } else { + return std::string("JSON field '") + key + "' must be an integer"; + } + *out = n; + return std::nullopt; +} + +std::optional OptionalNonNegativeSize( + const rapidjson::Value& obj, const char* key, std::size_t* out, + std::size_t def) +{ + if (!obj.HasMember(key)) { + *out = def; + return std::nullopt; + } + const auto& v = obj[key]; + if (v.IsUint64()) { + *out = static_cast(v.GetUint64()); + return std::nullopt; + } + if (v.IsInt64()) { + const int64_t n = v.GetInt64(); + if (n < 0) { + return std::string("JSON field '") + key + "' must be non-negative"; + } + *out = static_cast(n); + return std::nullopt; + } + return std::string("JSON field '") + key + "' must be an integer"; +} + +} // namespace + +std::optional LoadDatabaseConfigFromJsonFile(const std::string& path, DatabaseConfig* out) +{ + std::string raw; + if (auto e = ReadEntireFile(path, &raw)) { + return e; + } + + rapidjson::Document doc; + doc.Parse(raw.c_str()); + if (doc.HasParseError()) { + return std::string("JSON parse error: ") +rapidjson::GetParseError_En(doc.GetParseError()) + " at offset " + std::to_string(doc.GetErrorOffset()); + } + if (!doc.IsObject()) { + return std::string("root JSON value must be an object"); + } + + DatabaseConfig c; + if (auto e = ExpectString(doc, "databaseIp", &c.database_ip, false)) return e; + + while (!c.database_ip.empty() && std::isspace(static_cast(c.database_ip.front()))) c.database_ip.erase(0, 1); + + while (!c.database_ip.empty() && std::isspace(static_cast(c.database_ip.back()))) c.database_ip.pop_back(); + + if (auto e = OptionalIntFlexible(doc, "databasePort", &c.database_port, 3306)) return e; + + if (c.database_port < 1 || c.database_port > 65535) return std::string("databasePort must be between 1 and 65535"); + + if (auto e = ExpectString(doc, "odbcDriverName", &c.odbc_driver_name, false)) return e; + + if (auto e = ExpectString(doc, "primaryDSNName", &c.primary_dsn_name, true)) return e; + + if (auto e = ExpectString(doc, "secondaryDSNName", &c.secondary_dsn_name, false)) return e; + + if (auto e = ExpectString(doc, "dsnUserName", &c.dsn_user_name, true)) return e; + + if (auto e = ExpectString(doc, "dsnUserPassword", &c.dsn_user_password, true)) return e; + + int dc_id_int = 0; + if (auto e = OptionalIntFlexible(doc, "dcId", &dc_id_int, 0)) return e; + if (dc_id_int < INT32_MIN || dc_id_int > INT32_MAX) return std::string("dcId is out of range for int32_t"); + c.dc_id = static_cast(dc_id_int); + + if (auto e = OptionalInt(doc, "queryRetryCount", &c.query_retry_count, 3)) return e; + + if (auto e = OptionalNonNegativeSize(doc, "minPoolConnections", &c.min_pool_connections, 2)) return e; + + if (auto e = OptionalNonNegativeSize(doc, "maxPoolConnections", &c.max_pool_connections, 5)) return e; + + if (c.min_pool_connections < 1) return std::string("minPoolConnections must be at least 1"); + + if (c.max_pool_connections < c.min_pool_connections) return std::string("maxPoolConnections must be greater than or equal to minPoolConnections"); + *out = std::move(c); + return std::nullopt; +} + +}} // namespace triton::server + diff --git a/src/database_config.h b/src/database_config.h new file mode 100644 index 0000000000..c1083ed020 --- /dev/null +++ b/src/database_config.h @@ -0,0 +1,56 @@ +// Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of NVIDIA CORPORATION nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +// OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#pragma once + +#include +#include +#include +#include + +namespace triton { namespace server { + +inline constexpr const char kTritonDmConfigJsonPath[] = "/etc/triton-dmconfig.json"; + +struct DatabaseConfig { + std::string database_ip; + int database_port{3306}; + std::string odbc_driver_name; + + std::string primary_dsn_name; + std::string secondary_dsn_name; + std::string dsn_user_name; + std::string dsn_user_password; + + int32_t dc_id{0}; + int query_retry_count{3}; + std::size_t min_pool_connections{2}; + std::size_t max_pool_connections{5}; +}; + +std::optional LoadDatabaseConfigFromJsonFile(const std::string& path, DatabaseConfig* out); + +}} // namespace triton::server + diff --git a/src/main.cc b/src/main.cc index b2eee17c68..38b21ec648 100644 --- a/src/main.cc +++ b/src/main.cc @@ -42,11 +42,20 @@ #include #include +#include +#include +#include #include #include +#include +#include #include #include +#include "database_config.h" +#ifdef TRITON_ENABLE_MYSQL_ODBC +#include "mysql_odbc_connection_pool.h" +#endif // TRITON_ENABLE_MYSQL_ODBC #include "triton_signal.h" #ifdef TRITON_ENABLE_ASAN @@ -103,6 +112,113 @@ std::unique_ptr g_vertex_ai_service; triton::server::TritonServerParameters g_triton_params; +// Populated at startup when /etc/triton-dmconfig.json is present. +std::optional g_triton_dm_database_config; + +#ifdef TRITON_ENABLE_MYSQL_ODBC +// ODBC pool opened after successful config load (same lifetime as the process). +std::unique_ptr g_triton_dm_odbc_pool; + +// Registered with std::atexit after the pool is initialized so connections are +// released on normal return, exit(), and FAIL_IF_ERR paths. +extern "C" void TritonDmOdbcPoolAtExit(void) +{ + triton::server::SetGlobalMysqlOdbcPool(nullptr); + g_triton_dm_odbc_pool.reset(); +} + +void TritonModelsRefreshThreadMain() +{ + using std::chrono_literals::operator""min; + while (!triton::server::signal_exiting_) { + if (auto err = triton::server::UpdateTritonModelsData()) { +#ifdef TRITON_ENABLE_LOGGING + LOG_WARNING << "Triton ML models DB refresh failed: " << *err; +#endif // TRITON_ENABLE_LOGGING + } + std::unique_lock lock(triton::server::signal_exit_mu_); + triton::server::signal_exit_cv_.wait_for(lock, 15min, [] { return triton::server::signal_exiting_; }); + } +} + +std::optional g_triton_models_refresh_thread; + +void StartTritonModelsRefreshThread() +{ + if (!g_triton_dm_odbc_pool) { + return; + } + g_triton_models_refresh_thread.emplace(TritonModelsRefreshThreadMain); +} + +void JoinTritonModelsRefreshThread() +{ + if (!g_triton_models_refresh_thread.has_value()) { + return; + } + if (g_triton_models_refresh_thread->joinable()) { + { + std::lock_guard lock(triton::server::signal_exit_mu_); + triton::server::signal_exit_cv_.notify_all(); + } + g_triton_models_refresh_thread->join(); + } + g_triton_models_refresh_thread.reset(); +} +#endif // TRITON_ENABLE_MYSQL_ODBC + +void LoadTritonDmDatabaseConfigAtStartup() +{ + namespace fs = std::filesystem; + const std::string path(triton::server::kTritonDmConfigJsonPath); + if (!fs::exists(path)) { +#ifdef TRITON_ENABLE_LOGGING + LOG_WARNING << "Database config file '" << path << "' not found; continuing without DM database metadata."; +#else + std::cerr << "warning: database config file '" << path << "' not found; continuing without DM database metadata." << std::endl; +#endif // TRITON_ENABLE_LOGGING + return; + } + + triton::server::DatabaseConfig cfg; + if (auto err = triton::server::LoadDatabaseConfigFromJsonFile(path, &cfg)) { +#ifdef TRITON_ENABLE_LOGGING + LOG_ERROR << "Failed to load '" << path << "': " << *err; +#else + std::cerr << "Failed to load '" << path << "': " << *err << std::endl; +#endif // TRITON_ENABLE_LOGGING + exit(1); + } + + g_triton_dm_database_config = std::move(cfg); + +#ifdef TRITON_ENABLE_LOGGING + LOG_INFO << "Loaded database config from '" << path << "' (databaseIp='" << g_triton_dm_database_config->database_ip << "', databasePort=" << g_triton_dm_database_config->database_port << ")"; +#endif // TRITON_ENABLE_LOGGING + +#ifdef TRITON_ENABLE_MYSQL_ODBC + g_triton_dm_odbc_pool = std::make_unique(*g_triton_dm_database_config); + if (auto err = g_triton_dm_odbc_pool->Initialize()) { +#ifdef TRITON_ENABLE_LOGGING + LOG_ERROR << "Failed to initialize MySQL ODBC connection pool: " << *err; +#else + std::cerr << "Failed to initialize MySQL ODBC connection pool: " << *err << std::endl; +#endif // TRITON_ENABLE_LOGGING + g_triton_dm_odbc_pool.reset(); + exit(1); + } + if (std::atexit(TritonDmOdbcPoolAtExit) != 0) { +#ifdef TRITON_ENABLE_LOGGING + LOG_WARNING << "std::atexit failed; ODBC pool will still be destroyed at static exit"; +#endif // TRITON_ENABLE_LOGGING + } + triton::server::SetGlobalMysqlOdbcPool(g_triton_dm_odbc_pool.get()); +#ifdef TRITON_ENABLE_LOGGING + LOG_INFO << "MySQL ODBC connection pool started ("<< g_triton_dm_database_config->max_pool_connections << " connections to DSN '" << g_triton_dm_database_config->primary_dsn_name << "')"; +#endif // TRITON_ENABLE_LOGGING +#endif // TRITON_ENABLE_MYSQL_ODBC +} + #ifdef TRITON_ENABLE_GRPC TRITONSERVER_Error* StartGrpcService( @@ -469,6 +585,8 @@ main(int argc, char** argv) LOG_SET_OUT_FILE(g_triton_params.log_file_); #endif // TRITON_ENABLE_LOGGING + LoadTritonDmDatabaseConfigAtStartup(); + // Trace manager. triton::server::TraceManager* trace_manager; @@ -501,6 +619,10 @@ main(int argc, char** argv) exit(1); } +#ifdef TRITON_ENABLE_MYSQL_ODBC + StartTritonModelsRefreshThread(); +#endif // TRITON_ENABLE_MYSQL_ODBC + // Wait until a signal terminates the server... while (!triton::server::signal_exiting_) { // If enabled, poll the model repository to see if there have been @@ -521,6 +643,10 @@ main(int argc, char** argv) triton::server::signal_exit_cv_.wait_for(lock, wait_timeout); } +#ifdef TRITON_ENABLE_MYSQL_ODBC + JoinTritonModelsRefreshThread(); +#endif // TRITON_ENABLE_MYSQL_ODBC + // Stop the HTTP[, gRPC, and metrics] endpoints, and update exit timeout. uint32_t exit_timeout_secs = g_triton_params.exit_timeout_secs_; StopEndpoints(&exit_timeout_secs); diff --git a/src/mysql_odbc_connection_pool.cc b/src/mysql_odbc_connection_pool.cc new file mode 100644 index 0000000000..a563dc7629 --- /dev/null +++ b/src/mysql_odbc_connection_pool.cc @@ -0,0 +1,697 @@ +// Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of NVIDIA CORPORATION nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +// OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "mysql_odbc_connection_pool.h" + +#ifdef TRITON_ENABLE_LOGGING +#include "triton/common/logging.h" +#endif + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace triton { namespace server { + +namespace { + +std::atomic g_global_mysql_odbc_pool{nullptr}; + +std::string BuildMySqlDriverConnectString(const DatabaseConfig& c) +{ + std::string driver = c.odbc_driver_name; + if (driver.empty()) { + driver = "MySQL ODBC 9.7 Unicode Driver"; + } else if (driver == "MySQL ODBC 8.0 Unicode Driver") { + driver = "MySQL ODBC 9.7 Unicode Driver"; + } + std::ostringstream conn; + conn << "DRIVER={" << driver << "};" << "SERVER=" << c.database_ip << ";" << "PORT=" << c.database_port << ";" << "UID={" << c.dsn_user_name << "};" << "PWD={" << c.dsn_user_password << "};"; + return conn.str(); +} + +} // namespace + +PooledOdbcConnection::PooledOdbcConnection() = default; +PooledOdbcConnection::PooledOdbcConnection(MysqlOdbcConnectionPool* pool, SQLHDBC dbc) : pool_(pool), dbc_(dbc){} +PooledOdbcConnection::~PooledOdbcConnection() +{ + Release(); +} + +PooledOdbcConnection::PooledOdbcConnection(PooledOdbcConnection&& other) noexcept : pool_(other.pool_), dbc_(other.dbc_) +{ + other.pool_ = nullptr; + other.dbc_ = SQL_NULL_HDBC; +} + +PooledOdbcConnection& PooledOdbcConnection::operator=(PooledOdbcConnection&& other) noexcept +{ + if (this != &other) { + Release(); + pool_ = other.pool_; + dbc_ = other.dbc_; + other.pool_ = nullptr; + other.dbc_ = SQL_NULL_HDBC; + } + return *this; +} + +void PooledOdbcConnection::Release() +{ + if (pool_ != nullptr && dbc_ != SQL_NULL_HDBC) { + pool_->ReturnConnection(dbc_); + } + pool_ = nullptr; + dbc_ = SQL_NULL_HDBC; +} + +MysqlOdbcConnectionPool::MysqlOdbcConnectionPool(DatabaseConfig config) : config_(std::move(config)){} +MysqlOdbcConnectionPool::~MysqlOdbcConnectionPool() +{ + std::lock_guard lk(mu_); + for (SQLHDBC dbc : all_handles_) { + if (dbc != SQL_NULL_HDBC) { + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + } + } + all_handles_.clear(); + free_.clear(); + if (henv_ != SQL_NULL_HENV) { + SQLFreeHandle(SQL_HANDLE_ENV, henv_); + henv_ = SQL_NULL_HENV; + } +} + +std::optional MysqlOdbcConnectionPool::Initialize() +{ + auto cleanup_partial = [this]() { + for (SQLHDBC dbc : all_handles_) { + if (dbc != SQL_NULL_HDBC) { + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + } + } + all_handles_.clear(); + free_.clear(); + if (henv_ != SQL_NULL_HENV) { + SQLFreeHandle(SQL_HANDLE_ENV, henv_); + henv_ = SQL_NULL_HENV; + } + }; + + SQLRETURN rc = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &henv_); + if (!SQL_SUCCEEDED(rc)) { + return std::string("SQLAllocHandle(ENV) failed"); + } + + rc = SQLSetEnvAttr(henv_, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0); + if (!SQL_SUCCEEDED(rc)) { + SQLFreeHandle(SQL_HANDLE_ENV, henv_); + henv_ = SQL_NULL_HENV; + return std::string("SQLSetEnvAttr failed"); + } + + const std::size_t pool_size = config_.max_pool_connections; + all_handles_.reserve(pool_size); + + for (std::size_t i = 0; i < pool_size; ++i) { + SQLHDBC dbc = SQL_NULL_HDBC; + rc = SQLAllocHandle(SQL_HANDLE_DBC, henv_, &dbc); + if (!SQL_SUCCEEDED(rc)) { + cleanup_partial(); + return std::string("SQLAllocHandle(DBC) failed"); + } + + if (!config_.database_ip.empty()) { + const std::string conn_str = BuildMySqlDriverConnectString(config_); + SQLCHAR out_conn[1024]{}; + SQLSMALLINT out_conn_len = 0; + rc = SQLDriverConnect(dbc, nullptr, reinterpret_cast(const_cast(conn_str.data())), + SQL_NTS, out_conn, sizeof(out_conn), &out_conn_len, SQL_DRIVER_NOPROMPT); + if (!SQL_SUCCEEDED(rc)) { + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + cleanup_partial(); + return std::string("SQLDriverConnect failed"); + } + } else { + rc = SQLConnect(dbc,reinterpret_cast(const_cast(config_.primary_dsn_name.data())), + SQL_NTS, reinterpret_cast(const_cast(config_.dsn_user_name.data())), + SQL_NTS, reinterpret_cast(const_cast(config_.dsn_user_password.data())), + SQL_NTS); + if (!SQL_SUCCEEDED(rc)) { + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + cleanup_partial(); + return std::string("SQLConnect failed"); + } + } + + all_handles_.push_back(dbc); + free_.push_back(dbc); + } + + return std::nullopt; +} + +PooledOdbcConnection MysqlOdbcConnectionPool::Acquire() +{ + std::unique_lock lk(mu_); + cv_.wait(lk, [this] { return !free_.empty(); }); + SQLHDBC dbc = free_.front(); + free_.pop_front(); + return PooledOdbcConnection(this, dbc); +} + +void MysqlOdbcConnectionPool::ReturnConnection(SQLHDBC dbc) +{ + { + std::lock_guard lk(mu_); + free_.push_back(dbc); + } + cv_.notify_one(); +} + +void SetGlobalMysqlOdbcPool(MysqlOdbcConnectionPool* pool) +{ + g_global_mysql_odbc_pool.store(pool, std::memory_order_release); +} + +MysqlOdbcConnectionPool* GlobalMysqlOdbcPool() +{ + return g_global_mysql_odbc_pool.load(std::memory_order_acquire); +} + +namespace { + + +constexpr const char kSqlBtModelsMaxTs[] = "SELECT UNIX_TIMESTAMP(MAX(update_timestamp)) FROM MLBasedThrottling.lightgbm_bt_models WHERE on_off = 1"; +constexpr const char kSqlBtModelsForDc[] = "SELECT campaign_id, LOWER(model_name), feature_mapping, feature_sequence, applicable_campaigns FROM MLBasedThrottling.lightgbm_bt_models WHERE on_off = 1 AND dc_id = ?"; +constexpr size_t kModelNameBuf = kMaxModelNameLen + 1; +constexpr size_t kFeatureMappingBuf = kMaxFeatureMappingJsonLen + 1; +constexpr size_t kFeatureSequenceBuf = kMaxFeatureSequenceLen + 1; +constexpr size_t kApplicableCampaignsBuf = kMaxApplicableCampaignsLen + 1; + +struct OdbcStmt { + SQLHSTMT h{SQL_NULL_HSTMT}; + explicit OdbcStmt(SQLHDBC dbc) + { + const SQLRETURN rc = SQLAllocHandle(SQL_HANDLE_STMT, dbc, &h); + if (!SQL_SUCCEEDED(rc)) { + h = SQL_NULL_HSTMT; + } + } + ~OdbcStmt() + { + if (h != SQL_NULL_HSTMT) { + SQLFreeHandle(SQL_HANDLE_STMT, h); + } + } + OdbcStmt(const OdbcStmt&) = delete; + OdbcStmt& operator=(const OdbcStmt&) = delete; +}; + +void Trim(std::string* s) +{ + if (s == nullptr || s->empty()) { + return; + } + const auto not_space = [](unsigned char c) { return !std::isspace(c); }; + auto b = std::find_if(s->begin(), s->end(), not_space); + auto e = std::find_if(s->rbegin(), s->rend(), not_space).base(); + if (b >= e) { + s->clear(); + } else { + *s = std::string(b, e); + } +} + +std::string SqlCharBufferToString(const std::vector& buf, SQLLEN cb) +{ + if (cb == SQL_NULL_DATA) { + return {}; + } + const char* p = reinterpret_cast(buf.data()); + if (cb < 0) { + return std::string(p); + } + return std::string(p, static_cast(cb)); +} + +std::string JsonScalarToString(const rapidjson::Value& v) +{ + if (v.IsString()) { + return std::string(v.GetString(), v.GetStringLength()); + } + if (v.IsBool()) { + return v.GetBool() ? "true" : "false"; + } + if (v.IsInt()) { + return std::to_string(v.GetInt()); + } + if (v.IsUint()) { + return std::to_string(v.GetUint()); + } + if (v.IsInt64()) { + return std::to_string(v.GetInt64()); + } + if (v.IsUint64()) { + return std::to_string(v.GetUint64()); + } + if (v.IsDouble()) { + return std::to_string(v.GetDouble()); + } + if (v.IsNull()) { + return {}; + } + return {}; +} + +std::optional FetchMaxUnixTimestampFromDbc(SQLHDBC dbc, const char* sql, int64_t* out_ts) +{ + *out_ts = 0; + OdbcStmt st(dbc); + if (st.h == SQL_NULL_HSTMT) { + return std::string("SQLAllocHandle(STMT) failed"); + } + SQLRETURN rc = SQLExecDirect(st.h, reinterpret_cast(const_cast(sql)), SQL_NTS); + if (!SQL_SUCCEEDED(rc)) { + return std::string("SQLExecDirect failed"); + } + int64_t ts = 0; + SQLLEN cb_ts = 0; + rc = SQLBindCol(st.h, 1, SQL_C_SBIGINT, &ts, 0, &cb_ts); + if (!SQL_SUCCEEDED(rc)) { + return std::string("SQLBindCol failed"); + } + rc = SQLFetch(st.h); + if (rc == SQL_NO_DATA) { + return std::nullopt; + } + if (!SQL_SUCCEEDED(rc)) { + return std::string("SQLFetch failed"); + } + if (cb_ts != SQL_NULL_DATA) { + *out_ts = ts; + } + return std::nullopt; +} + +} // namespace + +void SplitCommaSeparatedStrings(const std::string& s, std::vector* out) +{ + out->clear(); + std::size_t start = 0; + while (start < s.size()) { + const std::size_t comma = s.find(',', start); + std::string piece = (comma == std::string::npos) ? s.substr(start) : s.substr(start, comma - start); + Trim(&piece); + if (!piece.empty()) { + out->push_back(std::move(piece)); + } + if (comma == std::string::npos) { + break; + } + start = comma + 1; + } +} + +std::optional ParseApplicableCampaignIds(const std::string& s, std::vector* out) +{ + out->clear(); + std::string t = s; + Trim(&t); + if (t.empty()) { + return std::nullopt; + } + if (t.size() == 2 && (t[0] == 'n' || t[0] == 'N') && (t[1] == 'a' || t[1] == 'A')) { + return std::nullopt; + } + + std::vector tokens; + SplitCommaSeparatedStrings(t, &tokens); + if (tokens.empty()) { + return std::nullopt; + } + + for (const auto& tok : tokens) { + char* endptr = nullptr; + const long v = std::strtol(tok.c_str(), &endptr, 10); + if (endptr == tok.c_str() || *endptr != '\0') { + return std::string("invalid campaign id token: ") + tok; + } + out->push_back(static_cast(v)); + } + return std::nullopt; +} + +int GetFeatureMappingIdx(const char* feature_name, const char* feature, const FeatureMappingTables* feature_mapping) +{ + if (feature_mapping == nullptr || feature_name == nullptr || feature == nullptr) { + return -1; + } + const auto outer = feature_mapping->find(feature_name); + if (outer == feature_mapping->end()) { + return -1; + } + const auto inner = outer->second.value_to_index.find(feature); + if (inner == outer->second.value_to_index.end()) { + return -1; + } + return inner->second; +} + +bool ParseFeatureMappingJson(const std::string& json, FeatureMappingTables* out, std::string* parse_error) +{ + out->clear(); + if (parse_error != nullptr) { + parse_error->clear(); + } + rapidjson::Document doc; + doc.Parse(json.c_str()); + if (doc.HasParseError()) { + if (parse_error != nullptr) { + *parse_error = std::string("JSON parse error at offset ") + std::to_string(doc.GetErrorOffset()) + ": " + rapidjson::GetParseError_En(doc.GetParseError()); + } + return false; + } + if (!doc.IsObject()) { + if (parse_error != nullptr) { + *parse_error = "feature_mapping root must be a JSON object"; + } + return false; + } + + for (auto it = doc.MemberBegin(); it != doc.MemberEnd(); ++it) { + if (!it->name.IsString()) { + continue; + } + const std::string feature_name(it->name.GetString(), it->name.GetStringLength()); + if (feature_name.size() > kTritonFeatureMappingMaxTokenLen) { + if (parse_error != nullptr) { + *parse_error = "feature name exceeds TRITON_FEATURE_MAPPING_BUFF_SIZE-1"; + } + return false; + } + if (!it->value.IsArray()) { + if (parse_error != nullptr) { + *parse_error = "feature '" + feature_name + "' value is not a JSON array"; + } + return false; + } + const rapidjson::Value& arr = it->value; + FeatureValueIndexMap table; + table.values.reserve(arr.Size()); + for (rapidjson::SizeType i = 0; i < arr.Size(); ++i) { + const std::string cell = JsonScalarToString(arr[i]); + if (cell.size() > kTritonFeatureMappingMaxTokenLen) { + if (parse_error != nullptr) { + *parse_error = "categorical value exceeds TRITON_FEATURE_MAPPING_BUFF_SIZE-1 for feature '" + feature_name + "'"; + } + return false; + } + table.values.push_back(cell); + table.value_to_index[cell] = static_cast(i); + } + (*out)[feature_name] = std::move(table); + } + return true; +} + +std::optional FetchLightgbmBtModelsMaxUpdateUnixSeconds(int64_t* out_ts) +{ + MysqlOdbcConnectionPool* pool = GlobalMysqlOdbcPool(); + if (pool == nullptr) { + return std::string("MySQL ODBC pool is not registered; call SetGlobalMysqlOdbcPool after pool init"); + } + PooledOdbcConnection conn = pool->Acquire(); + if (!conn) { + return std::string("failed to acquire ODBC connection"); + } + return FetchMaxUnixTimestampFromDbc(conn.handle(), kSqlBtModelsMaxTs, out_ts); +} + +namespace { + +bool TryMergeRowIntoCampaignMap(const LightgbmBtModelRow& row, CampaignToFeatureMappings& out) +{ + const CampaignBtModelBundle bundle{row.model_name, row.feature_mapping, row.feature_sequence}; + + if (row.campaign_id != 0) { + if (out.find(row.campaign_id) != out.end()) { +#ifdef TRITON_ENABLE_LOGGING + LOG_WARNING << "TRITON: campaign " << row.campaign_id << " already exists in triton models map; skipping row"; +#endif + return false; + } + out[row.campaign_id] = bundle; + return true; + } + + if (row.applicable_campaigns.empty()) { + if (out.find(0) != out.end()) { +#ifdef TRITON_ENABLE_LOGGING + LOG_WARNING << "TRITON: campaign 0 already exists in triton models map; skipping row"; +#endif + return false; + } + out[0] = bundle; + return true; + } + + bool any_inserted = false; + for (int32_t cid : row.applicable_campaigns) { + if (out.find(cid) != out.end()) { +#ifdef TRITON_ENABLE_LOGGING + LOG_WARNING << "TRITON: campaign " << cid << " already exists in triton models map"; +#endif + continue; + } + out[cid] = bundle; + any_inserted = true; + } + return any_inserted; +} + +} // namespace + +std::optional FetchLightgbmBtModelsForDc(CampaignToFeatureMappings& out_campaign_map) +{ + out_campaign_map.clear(); + MysqlOdbcConnectionPool* pool = GlobalMysqlOdbcPool(); + if (pool == nullptr) { + return std::string("MySQL ODBC pool is not registered; call SetGlobalMysqlOdbcPool after pool init"); + } + const int32_t dc_id = pool->Config().dc_id; + PooledOdbcConnection conn = pool->Acquire(); + if (!conn) { + return std::string("failed to acquire ODBC connection"); + } + + OdbcStmt st(conn.handle()); + if (st.h == SQL_NULL_HSTMT) { + return std::string("SQLAllocHandle(STMT) failed"); + } + + SQLRETURN rc = SQLPrepare(st.h, reinterpret_cast(const_cast(kSqlBtModelsForDc)), SQL_NTS); + if (!SQL_SUCCEEDED(rc)) { + return std::string("SQLPrepare failed"); + } + + SQLINTEGER dc_param = static_cast(dc_id); + rc = SQLBindParameter(st.h, 1, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 0, 0, &dc_param, 0, nullptr); + if (!SQL_SUCCEEDED(rc)) { + return std::string("SQLBindParameter failed"); + } + + rc = SQLExecute(st.h); + if (!SQL_SUCCEEDED(rc)) { + return std::string("SQLExecute failed"); + } + + SQLINTEGER s_campaign_id = 0; + SQLLEN cb_campaign_id = 0; + std::vector model_buf(kModelNameBuf + 1, 0); + SQLLEN cb_model_name = 0; + std::vector mapping_buf(kFeatureMappingBuf + 1, 0); + SQLLEN cb_mapping = 0; + std::vector sequence_buf(kFeatureSequenceBuf + 1, 0); + SQLLEN cb_sequence = 0; + std::vector campaigns_buf(kApplicableCampaignsBuf + 1, 0); + SQLLEN cb_campaigns = 0; + + int col = 1; + rc = SQLBindCol(st.h, col++, SQL_C_SLONG, &s_campaign_id, 0, &cb_campaign_id); + if (!SQL_SUCCEEDED(rc)) { + return std::string("SQLBindCol(campaign_id) failed"); + } + rc = SQLBindCol(st.h, col++, SQL_C_CHAR, model_buf.data(), static_cast(model_buf.size()), &cb_model_name); + if (!SQL_SUCCEEDED(rc)) { + return std::string("SQLBindCol(model_name) failed"); + } + rc = SQLBindCol(st.h, col++, SQL_C_CHAR, mapping_buf.data(), static_cast(mapping_buf.size()), &cb_mapping); + if (!SQL_SUCCEEDED(rc)) { + return std::string("SQLBindCol(feature_mapping) failed"); + } + rc = SQLBindCol(st.h, col++, SQL_C_CHAR, sequence_buf.data(), static_cast(sequence_buf.size()), &cb_sequence); + if (!SQL_SUCCEEDED(rc)) { + return std::string("SQLBindCol(feature_sequence) failed"); + } + rc = SQLBindCol(st.h, col++, SQL_C_CHAR, campaigns_buf.data(), static_cast(campaigns_buf.size()), &cb_campaigns); + if (!SQL_SUCCEEDED(rc)) { + return std::string("SQLBindCol(applicable_campaigns) failed"); + } + + while (true) { + rc = SQLFetch(st.h); + if (rc == SQL_NO_DATA) { + break; + } + if (!SQL_SUCCEEDED(rc)) { + return std::string("SQLFetch failed"); + } + if (cb_campaign_id == SQL_NULL_DATA) { + continue; + } + + LightgbmBtModelRow row; + row.campaign_id = static_cast(s_campaign_id); + + if (cb_model_name != SQL_NULL_DATA) { + row.model_name = SqlCharBufferToString(model_buf, cb_model_name); + Trim(&row.model_name); + } + + std::string mapping_json; + if (cb_mapping != SQL_NULL_DATA) { + mapping_json = SqlCharBufferToString(mapping_buf, cb_mapping); + Trim(&mapping_json); + } + std::string parse_err; + if (!ParseFeatureMappingJson(mapping_json, &row.feature_mapping, &parse_err)) { + return std::string("feature_mapping JSON: ") + parse_err; + } + + std::string seq_str; + if (cb_sequence != SQL_NULL_DATA) { + seq_str = SqlCharBufferToString(sequence_buf, cb_sequence); + Trim(&seq_str); + } + SplitCommaSeparatedStrings(seq_str, &row.feature_sequence); + + std::string camp_str; + if (cb_campaigns != SQL_NULL_DATA) { + camp_str = SqlCharBufferToString(campaigns_buf, cb_campaigns); + Trim(&camp_str); + } + if (auto err = ParseApplicableCampaignIds(camp_str, &row.applicable_campaigns)) { + return err; + } + + if (!TryMergeRowIntoCampaignMap(row, out_campaign_map)) { + continue; + } + } + + return std::nullopt; +} + +namespace { + std::array g_triton_campaign_feature_mappings; + std::atomic g_triton_models_active{0}; + std::atomic g_triton_models_modification_time{0}; +} + +bool IsTritonModelsModified() +{ + MysqlOdbcConnectionPool* pool = GlobalMysqlOdbcPool(); + if (pool == nullptr) { + return false; + } + + int retry_count = pool->Config().query_retry_count; + if (retry_count < 0) { + retry_count = 0; + } + + int64_t last_updated = 0; + std::optional err; + do { + err = FetchLightgbmBtModelsMaxUpdateUnixSeconds(&last_updated); + if (!err) { + break; + } + } while (err.has_value() && retry_count--); + + if (err.has_value()) { + return false; + } + + const uint32_t lu = static_cast(last_updated); + const uint32_t prev = g_triton_models_modification_time.load(std::memory_order_relaxed); + if (lu > prev) { + g_triton_models_modification_time.store(lu, std::memory_order_relaxed); + return true; + } + return false; +} + +std::optional UpdateTritonModelsData() +{ + if (!IsTritonModelsModified()) { + return std::nullopt; + } + + CampaignToFeatureMappings by_campaign; + if (auto err = FetchLightgbmBtModelsForDc(by_campaign)) { + return err; + } + + const int idx = g_triton_models_active.load(std::memory_order_acquire) & 1; + const int inactive = 1 - idx; + g_triton_campaign_feature_mappings[inactive] = std::move(by_campaign); + g_triton_models_active.store(inactive, std::memory_order_release); + g_triton_campaign_feature_mappings[idx].clear(); + return std::nullopt; +} + +const CampaignToFeatureMappings* ActiveCampaignToFeatureMappings() +{ + const int idx = g_triton_models_active.load(std::memory_order_acquire) & 1; + return &g_triton_campaign_feature_mappings[idx]; +} + +}} // namespace triton::server \ No newline at end of file diff --git a/src/mysql_odbc_connection_pool.h b/src/mysql_odbc_connection_pool.h new file mode 100644 index 0000000000..771fcefbfb --- /dev/null +++ b/src/mysql_odbc_connection_pool.h @@ -0,0 +1,163 @@ +// Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of NVIDIA CORPORATION nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +// OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Connection pool over the MySQL ODBC driver using the ODBC API (unixODBC / +// iODBC). Build with -DTRITON_ENABLE_MYSQL_ODBC=ON and install unixodbc-dev +// (Debian/Ubuntu) plus a configured MySQL ODBC DSN. + +#pragma once + +#include "database_config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + +#include +#include + +namespace triton { namespace server { + +inline constexpr std::size_t kMaxModelNameLen = 100; +inline constexpr std::size_t kMaxFeatureMappingJsonLen = 16 * (1 << 20); +inline constexpr std::size_t kMaxFeatureSequenceLen = 1000; +inline constexpr std::size_t kMaxApplicableCampaignsLen = 4096; +inline constexpr std::size_t kMaxLightgbmUpdateTimestampLen = 256; +inline constexpr std::size_t kTritonFeatureMappingBuffSize = 256; +inline constexpr std::size_t kTritonFeatureMappingMaxTokenLen = + kTritonFeatureMappingBuffSize - 1; + +class MysqlOdbcConnectionPool; + +class PooledOdbcConnection { + public: + PooledOdbcConnection(); + ~PooledOdbcConnection(); + + PooledOdbcConnection(PooledOdbcConnection&& other) noexcept; + PooledOdbcConnection& operator=(PooledOdbcConnection&& other) noexcept; + + PooledOdbcConnection(const PooledOdbcConnection&) = delete; + PooledOdbcConnection& operator=(const PooledOdbcConnection&) = delete; + + SQLHDBC handle() const { return dbc_; } + explicit operator bool() const { return dbc_ != SQL_NULL_HDBC; } + + private: + friend class MysqlOdbcConnectionPool; + PooledOdbcConnection(MysqlOdbcConnectionPool* pool, SQLHDBC dbc); + + void Release(); + + MysqlOdbcConnectionPool* pool_{nullptr}; + SQLHDBC dbc_{SQL_NULL_HDBC}; +}; + +class MysqlOdbcConnectionPool { + public: + explicit MysqlOdbcConnectionPool(DatabaseConfig config); + ~MysqlOdbcConnectionPool(); + + MysqlOdbcConnectionPool(const MysqlOdbcConnectionPool&) = delete; + MysqlOdbcConnectionPool& operator=(const MysqlOdbcConnectionPool&) = delete; + + std::optional Initialize(); + + PooledOdbcConnection Acquire(); + + const DatabaseConfig& Config() const { return config_; } + + private: + friend class PooledOdbcConnection; + void ReturnConnection(SQLHDBC dbc); + + DatabaseConfig config_; + SQLHENV henv_{SQL_NULL_HENV}; + std::vector all_handles_; + std::deque free_; + std::mutex mu_; + std::condition_variable cv_; +}; + +void SetGlobalMysqlOdbcPool(MysqlOdbcConnectionPool* pool); +MysqlOdbcConnectionPool* GlobalMysqlOdbcPool(); + +struct FeatureValueIndexMap { + std::vector values; + std::unordered_map value_to_index; +}; + +using FeatureMappingTables = std::unordered_map; + +// Legacy get_feature_mapping_idx: look up categorical index for `feature` +// (token string) under column `feature_name`. Returns -1 if any map or key is +// missing, or if `feature_name` / `feature` / `feature_mapping` is null. +int GetFeatureMappingIdx( + const char* feature_name, const char* feature, + const FeatureMappingTables* feature_mapping); + +// Loaded from `lightgbm_bt_models` per campaign_id (after merge rules). +struct CampaignBtModelBundle { + std::string model_name; + FeatureMappingTables feature_mapping; + std::vector feature_sequence; +}; + +using CampaignToFeatureMappings = std::unordered_map; + +struct LightgbmBtModelRow { + int32_t campaign_id{0}; + std::string model_name; + FeatureMappingTables feature_mapping; + std::vector feature_sequence; + std::vector applicable_campaigns; +}; + +std::optional FetchLightgbmFeatureMappingMaxUpdateUnixSeconds(int64_t* out_ts); +std::optional FetchLightgbmBtModelsMaxUpdateUnixSeconds(int64_t* out_ts); + +std::optional FetchLightgbmBtModelsForDc(CampaignToFeatureMappings& out_campaign_map); +bool ParseFeatureMappingJson(const std::string& json, FeatureMappingTables* out, std::string* parse_error); +void SplitCommaSeparatedStrings(const std::string& s, std::vector* out_tokens); +std::optional ParseApplicableCampaignIds(const std::string& s, std::vector* out_ids); +bool IsTritonModelsModified(); +std::optional UpdateTritonModelsData(); +const CampaignToFeatureMappings* ActiveCampaignToFeatureMappings(); + +}} // namespace triton::server + From 9ba4580a921d7444476e7d40f81f8688f5996d27 Mon Sep 17 00:00:00 2001 From: "shantanu.mirajgave" Date: Mon, 15 Jun 2026 07:19:48 -0700 Subject: [PATCH 229/230] Added logic for feature mapping. Converting request to triton request and fetching recommendations --- Dockerfile | 16 + Dockerfile.pubmatic-bt-tritonserver | 52 +++ build.py | 57 ++- src/CMakeLists.txt | 64 +++ src/http_server.cc | 670 +--------------------------- src/http_server_macros.h | 41 ++ src/multi_infer.cc | 635 ++++++++++++++++++++++++++ src/transform.cc | 343 ++++++++++++++ src/transform.h | 71 +++ 9 files changed, 1284 insertions(+), 665 deletions(-) create mode 100644 Dockerfile.pubmatic-bt-tritonserver create mode 100644 src/http_server_macros.h create mode 100644 src/multi_infer.cc create mode 100644 src/transform.cc create mode 100644 src/transform.h diff --git a/Dockerfile b/Dockerfile index f9cf801b81..27454c0d38 100644 --- a/Dockerfile +++ b/Dockerfile @@ -70,6 +70,22 @@ RUN set -eux; \ rm -f "/tmp/${DEB}"; \ rm -rf /var/lib/apt/lists/* +# mysql-connector-odbc registers Driver=/usr/lib/.../odbc/libmyodbc9w.so in +# /etc/odbcinst.ini, but Ubuntu/Debian often install the .so under +# /usr/lib//odbc/ only. unixODBC then fails with "Can't open lib +# '/usr/lib/odbc/libmyodbc9w.so'". Symlink all libmyodbc*.so into /usr/lib/odbc/. +RUN set -eux; \ + mkdir -p /usr/lib/odbc; \ + for f in \ + /usr/lib/x86_64-linux-gnu/odbc/libmyodbc*.so \ + /usr/lib/aarch64-linux-gnu/odbc/libmyodbc*.so; \ + do \ + if [ -f "${f}" ]; then \ + ln -sf "${f}" "/usr/lib/odbc/$(basename "${f}")"; \ + fi; \ + done; \ + test -f /usr/lib/odbc/libmyodbc9w.so + # Optional DSN file (only used when databaseIp is empty in triton-dmconfig.json). COPY replace-artifacts/odbc.ini /etc/odbc.ini RUN chmod 644 /etc/odbc.ini diff --git a/Dockerfile.pubmatic-bt-tritonserver b/Dockerfile.pubmatic-bt-tritonserver new file mode 100644 index 0000000000..340c3b27b5 --- /dev/null +++ b/Dockerfile.pubmatic-bt-tritonserver @@ -0,0 +1,52 @@ +# Copyright 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Extend the image produced by build.py (`docker build -t tritonserver`) with +# MySQL ODBC runtime, driver path symlinks for unixODBC, and config files. +# Build context: repository root (same as build.py final docker build). +# +# Prerequisites: +# - Image `tritonserver` must exist. +# - replace-artifacts/odbc.ini +# - replace-artifacts/triton-dmconfig.json +# +# Example: +# docker build -f Dockerfile.pubmatic-bt-tritonserver -t pubmatic-bt-tritonserver . + +FROM tritonserver + +ARG MYSQL_ODBC_DEB_VERSION=9.7.0-1ubuntu22.04 +ARG MYSQL_ODBC_DEB_ARCH=amd64 + +USER root + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + unixodbc \ + odbcinst; \ + DEB="mysql-connector-odbc_${MYSQL_ODBC_DEB_VERSION}_${MYSQL_ODBC_DEB_ARCH}.deb"; \ + curl -fsSL -o "/tmp/${DEB}" \ + "https://repo.mysql.com/apt/ubuntu/pool/mysql-tools/m/mysql-connector-odbc/${DEB}"; \ + apt-get install -y "/tmp/${DEB}" || apt-get -fy install; \ + rm -f "/tmp/${DEB}"; \ + rm -rf /var/lib/apt/lists/* + +RUN set -eux; \ + mkdir -p /usr/lib/odbc; \ + for f in \ + /usr/lib/x86_64-linux-gnu/odbc/libmyodbc*.so \ + /usr/lib/aarch64-linux-gnu/odbc/libmyodbc*.so; \ + do \ + if [ -f "${f}" ]; then \ + ln -sf "${f}" "/usr/lib/odbc/$(basename "${f}")"; \ + fi; \ + done; \ + test -f /usr/lib/odbc/libmyodbc9w.so + +COPY replace-artifacts/odbc.ini /etc/odbc.ini +RUN chmod 644 /etc/odbc.ini + +COPY replace-artifacts/triton-dmconfig.json /etc/triton-dmconfig.json +RUN chmod 644 /etc/triton-dmconfig.json diff --git a/build.py b/build.py index 5a0f96413a..6ea96f5218 100755 --- a/build.py +++ b/build.py @@ -498,6 +498,7 @@ def core_cmake_args(components, backends, cmake_dir, install_dir): cargs.append(cmake_core_enable("TRITON_ENABLE_ENSEMBLE", "ensemble" in backends)) cargs.append(cmake_core_enable("TRITON_ENABLE_TENSORRT", "tensorrt" in backends)) + cargs.append(cmake_core_enable("TRITON_ENABLE_MYSQL_ODBC", FLAGS.enable_mysql_odbc)) cargs += cmake_core_extra_args() cargs.append(cmake_dir) @@ -902,6 +903,12 @@ def install_dcgm_libraries(dcgm_version, target_machine): def create_dockerfile_buildbase_rhel(ddir, dockerfile_name, argmap): + buildbase_odbc_layer = "" + if FLAGS.enable_mysql_odbc: + buildbase_odbc_layer = """ +RUN yum install -y unixODBC-devel +""" + df = """ ARG TRITON_VERSION={} ARG TRITON_CONTAINER_VERSION={} @@ -959,6 +966,7 @@ def create_dockerfile_buildbase_rhel(ddir, dockerfile_name, argmap): xz-devel \\ zlib-devel """ + df += buildbase_odbc_layer if os.getenv("CCACHE_REMOTE_ONLY") and os.getenv("CCACHE_REMOTE_STORAGE"): df += """ RUN curl -k -s -L https://github.com/ccache/ccache/archive/refs/tags/v4.10.2.tar.gz -o /tmp/ccache.tar.gz \\ @@ -1051,6 +1059,9 @@ def create_dockerfile_buildbase(ddir, dockerfile_name, argmap): SHELL ["cmd", "/S", "/C"] """ else: + mysql_odbc_line = ( + " unixodbc-dev \\\n" if FLAGS.enable_mysql_odbc else "" + ) df += """ # Ensure apt-get won't prompt for selecting options ENV DEBIAN_FRONTEND=noninteractive @@ -1101,7 +1112,9 @@ def create_dockerfile_buildbase(ddir, dockerfile_name, argmap): libarchive-dev \\ libxml2-dev \\ libnuma-dev \\ - wget \\ +""" + df += mysql_odbc_line + df += """ wget \\ && rm -rf /var/lib/apt/lists/* RUN pip3 install --upgrade \\ @@ -1904,6 +1917,34 @@ def create_docker_build_script(script_name, container_install_dir, container_ci_ docker_script.cwd(THIS_SCRIPT_DIR) docker_script.cmd(finalargs, check_exitcode=True) + if ( + FLAGS.pubmatic_bt_tritonserver_image + and target_platform() != "windows" + and target_platform() != "rhel" + ): + docker_script.blankln() + docker_script.commentln(8) + docker_script.comment( + "Pubmatic BT image: extend tritonserver with MySQL ODBC and config files" + ) + docker_script.comment( + "Uses Dockerfile.pubmatic-bt-tritonserver; requires replace-artifacts/" + ) + docker_script.comment() + pubmatic_args = [ + "docker", + "build", + "-t", + "pubmatic-bt-tritonserver", + "-f", + os.path.join( + THIS_SCRIPT_DIR, "Dockerfile.pubmatic-bt-tritonserver" + ), + ".", + ] + docker_script.cwd(THIS_SCRIPT_DIR) + docker_script.cmd(pubmatic_args, check_exitcode=True) + # # CI base image... tritonserver_cibase # @@ -2616,6 +2657,20 @@ def enable_all(): required=False, help="Enable ARM MALI GPU support.", ) + parser.add_argument( + "--enable-mysql-odbc", + action="store_true", + required=False, + help="Build tritonserver with MySQL ODBC connection pool. For host builds install unixodbc-dev (Debian/Ubuntu) or unixODBC-devel (RHEL). Container builds add these when this flag is set.", + ) + parser.add_argument( + "--pubmatic-bt-tritonserver-image", + action="store_true", + required=False, + help='After building image "tritonserver", also build "pubmatic-bt-tritonserver" ' + "(Dockerfile.pubmatic-bt-tritonserver: ODBC driver + replace-artifacts/odbc.ini and " + "replace-artifacts/triton-dmconfig.json). Ubuntu/Debian-based container builds only.", + ) parser.add_argument( "--min-compute-capability", type=str, diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6ac4d61915..9445464ebc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -92,6 +92,13 @@ find_package(re2 REQUIRED) option(TRITON_ENABLE_MYSQL_ODBC "Build tritonserver with MySQL ODBC connection pool (requires ODBC::ODBC / unixODBC)" OFF) if(TRITON_ENABLE_MYSQL_ODBC) + if(UNIX AND NOT APPLE AND NOT WIN32) + message( + STATUS + "TRITON_ENABLE_MYSQL_ODBC: ensure unixODBC development packages are installed " + "(e.g. Debian/Ubuntu: unixodbc-dev; RHEL: unixODBC-devel) so CMake can find ODBC." + ) + endif() find_package(ODBC REQUIRED) message(STATUS "Using ODBC ${ODBC_VERSION}") endif() @@ -357,11 +364,13 @@ if(${TRITON_ENABLE_HTTP} list(APPEND HTTP_ENDPOINT_SRCS http_server.cc + multi_infer.cc orca_http.cc ) list(APPEND HTTP_ENDPOINT_HDRS http_server.h + http_server_macros.h orca_http.h ) @@ -759,6 +768,61 @@ if (NOT WIN32) RUNTIME DESTINATION bin ) + # + # transform (JSON -> RapidJSON, TRITONSERVER_Error reporting) + # + add_library(transform STATIC transform.cc transform.h) + + # Required when transform is linked into shared objects (e.g. py-bindings -> + # http-endpoint-library -> transform); otherwise relocations fail at link time. + set_target_properties( + transform + PROPERTIES + POSITION_INDEPENDENT_CODE ON + ) + + target_compile_features(transform PRIVATE cxx_std_${TRITON_MIN_CXX_STANDARD}) + if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + target_compile_options( + transform + PRIVATE + /W1 /D_WIN32_WINNT=0x0A00 /EHsc /Zc:preprocessor + ) + else() + target_compile_options( + transform + PRIVATE + -Wall -Wextra -Wno-type-limits -Wno-unused-parameter -Wno-deprecated-declarations -Werror + ) + endif() + + target_link_libraries( + transform + PRIVATE + triton-common-async-work-queue # from repo-common + triton-common-error # from repo-common + triton-common-json # RapidJSON from repo-common + triton-core-serverapi # from repo-core + triton-core-serverstub # from repo-core + ) + + if(TRITON_ENABLE_MYSQL_ODBC) + target_compile_definitions(transform PRIVATE TRITON_ENABLE_MYSQL_ODBC=1) + target_link_libraries(transform PRIVATE ODBC::ODBC) + endif() + + if((${TRITON_ENABLE_HTTP} OR ${TRITON_ENABLE_METRICS} OR + ${TRITON_ENABLE_SAGEMAKER} OR ${TRITON_ENABLE_VERTEX_AI}) AND + TARGET http-endpoint-library AND TARGET transform) + target_link_libraries(http-endpoint-library PRIVATE transform) + if(TRITON_ENABLE_MYSQL_ODBC) + target_compile_definitions( + http-endpoint-library + PRIVATE TRITON_ENABLE_MYSQL_ODBC=1 + ) + endif() + endif() + if(${TRITON_ENABLE_GPU}) # # memory_alloc example diff --git a/src/http_server.cc b/src/http_server.cc index ebffcf0a8a..845caf0569 100644 --- a/src/http_server.cc +++ b/src/http_server.cc @@ -40,14 +40,14 @@ #include #include #include - +#include "triton/common/triton_json.h" #include "classification.h" +#include "http_server_macros.h" #define TRITONJSON_STATUSTYPE TRITONSERVER_Error* #define TRITONJSON_STATUSRETURN(M) \ return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INTERNAL, (M).c_str()) #define TRITONJSON_STATUSSUCCESS nullptr -#include "triton/common/triton_json.h" namespace triton { namespace server { @@ -61,36 +61,6 @@ namespace triton { namespace server { } \ } while (false) -#define RETURN_AND_RESPOND_IF_ERR(REQ, X) \ - do { \ - TRITONSERVER_Error* err__ = (X); \ - if (err__ != nullptr) { \ - EVBufferAddErrorJson((REQ)->buffer_out, err__); \ - evhtp_send_reply((REQ), HttpCodeFromError(err__)); \ - TRITONSERVER_ErrorDelete(err__); \ - return; \ - } \ - } while (false) - -#define RETURN_AND_RESPOND_WITH_ERR(REQ, CODE, MSG) \ - do { \ - EVBufferAddErrorJson((REQ)->buffer_out, MSG); \ - evhtp_send_reply((REQ), CODE); \ - return; \ - } while (false) - -#define RETURN_AND_RESPOND_IF_RESTRICTED( \ - REQ, RESTRICTED_CATEGORY, RESTRICTED_APIS) \ - do { \ - auto const& is_restricted_api = \ - RESTRICTED_APIS.IsRestricted(RESTRICTED_CATEGORY); \ - auto const& restriction = RESTRICTED_APIS.Get(RESTRICTED_CATEGORY); \ - if (is_restricted_api && RespondIfRestricted(REQ, restriction)) { \ - return; \ - } \ - } while (false) - - namespace { int @@ -3619,632 +3589,6 @@ HTTPAPIServer::GenerateRequestClass::ExactMappingInput( return nullptr; // success } -namespace { - -constexpr size_t kMaxMultiInferRequests = 16; - -TRITONSERVER_Error* -ParseEvbufferToTritonJson( - evbuffer* buf, triton::common::TritonJson::Value* out) -{ - struct evbuffer_iovec* v = nullptr; - int v_idx = 0; - const size_t buffer_len = evbuffer_get_length(buf); - int n = evbuffer_peek(buf, -1, NULL, NULL, 0); - if (n > 0) { - v = static_cast( - alloca(sizeof(struct evbuffer_iovec) * n)); - if (evbuffer_peek(buf, -1, NULL, v, n) != n) { - return TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INTERNAL, - "Unexpected error getting request buffer"); - } - } - return EVBufferToJson(out, v, &v_idx, buffer_len, n); -} - -TRITONSERVER_Error* -CopyInferSlotBodyJson( - triton::common::TritonJson::Value& slot, - triton::common::TritonJson::Value* infer_json) -{ - *infer_json = triton::common::TritonJson::Value( - triton::common::TritonJson::ValueType::OBJECT); - { - triton::common::TritonJson::Value v; - if (slot.Find("id", &v)) { - RETURN_IF_ERR(infer_json->Add("id", std::move(v))); - } - } - { - triton::common::TritonJson::Value v; - if (!slot.Find("inputs", &v)) { - return TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INVALID_ARG, - "Each entry in 'requests' must contain an 'inputs' array"); - } - RETURN_IF_ERR(infer_json->Add("inputs", std::move(v))); - } - { - triton::common::TritonJson::Value v; - if (slot.Find("outputs", &v)) { - RETURN_IF_ERR(infer_json->Add("outputs", std::move(v))); - } - } - { - triton::common::TritonJson::Value v; - if (slot.Find("parameters", &v)) { - RETURN_IF_ERR(infer_json->Add("parameters", std::move(v))); - } - } - return nullptr; -} - -TRITONSERVER_Error* -GetModelVersionStringFromSlot( - triton::common::TritonJson::Value& slot, std::string* ver_out) -{ - ver_out->clear(); - triton::common::TritonJson::Value mv; - if (!slot.Find("model_version", &mv)) { - return nullptr; - } - if (mv.IsString()) { - const char* s; - size_t len; - RETURN_IF_ERR(mv.AsString(&s, &len)); - ver_out->assign(s, len); - return nullptr; - } - if (mv.IsNumber()) { - int64_t iv; - RETURN_IF_ERR(mv.AsInt(&iv)); - *ver_out = std::to_string(iv); - return nullptr; - } - return TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INVALID_ARG, - "'model_version' must be a string or integer"); -} - -class MultiInferAggregator - : public std::enable_shared_from_this { - private: - struct FinishPayload { - std::shared_ptr agg; - }; - - public: - MultiInferAggregator( - evhtp_request_t* req, size_t slot_count, evthr_t* reply_thread, - std::vector> irequests) - : req_(req), n_(slot_count), reply_thread_(reply_thread), - irequests_(std::move(irequests)), success_json_(slot_count), - error_text_(slot_count), have_error_(slot_count, 0) - { - } - - std::shared_ptr IrequestAt(size_t i) const - { - return irequests_[i]; - } - - void CancelAllSubRequests() - { - std::lock_guard lk(mu_); - if (cancel_sent_) { - return; - } - cancel_sent_ = true; - for (auto& ir : irequests_) { - if (ir != nullptr) { - LOG_TRITONSERVER_ERROR( - TRITONSERVER_InferenceRequestCancel(ir.get()), - "cancelling multi_infer sub-request"); - } - } - } - - void OnShardDone( - size_t slot, TRITONSERVER_Error* finalize_err, - const std::string& response_json) - { - std::shared_ptr self; - { - std::lock_guard lk(mu_); - if (finalize_err != nullptr) { - have_error_[slot] = 1; - error_text_[slot] = TRITONSERVER_ErrorMessage(finalize_err); - TRITONSERVER_ErrorDelete(finalize_err); - if (!cancel_sent_) { - cancel_sent_ = true; - for (auto& ir : irequests_) { - if (ir != nullptr) { - LOG_TRITONSERVER_ERROR( - TRITONSERVER_InferenceRequestCancel(ir.get()), - "cancelling multi_infer sub-request"); - } - } - } - } else { - success_json_[slot] = response_json; - } - done_count_++; - if (done_count_ < n_ || reply_scheduled_) { - return; - } - reply_scheduled_ = true; - self = shared_from_this(); - } - - auto* fp = new FinishPayload{std::move(self)}; - evthr_defer(reply_thread_, FinishThunk, fp); - } - - private: - static void - FinishThunk(evthr_t* /*thr*/, void* arg, void* /*shared*/) - { - std::unique_ptr fp(static_cast(arg)); - fp->agg->WriteHttpReply(); - } - - void - WriteHttpReply() - { - triton::common::TritonJson::Value root( - triton::common::TritonJson::ValueType::OBJECT); - triton::common::TritonJson::Value responses( - root, triton::common::TritonJson::ValueType::ARRAY); - for (size_t i = 0; i < n_; ++i) { - if (have_error_[i]) { - triton::common::TritonJson::Value item( - root, triton::common::TritonJson::ValueType::OBJECT); - triton::common::TritonJson::Value err_part( - root, triton::common::TritonJson::ValueType::OBJECT); - TRITONSERVER_Error* ae = - err_part.AddString("message", error_text_[i]); - if (ae != nullptr) { - LOG_TRITONSERVER_ERROR(ae, "multi_infer: building error JSON"); - TRITONSERVER_ErrorDelete(ae); - } - TRITONSERVER_Error* be = item.Add("error", std::move(err_part)); - if (be != nullptr) { - LOG_TRITONSERVER_ERROR(be, "multi_infer: building error JSON"); - TRITONSERVER_ErrorDelete(be); - } - TRITONSERVER_Error* ce = responses.Append(std::move(item)); - if (ce != nullptr) { - LOG_TRITONSERVER_ERROR(ce, "multi_infer: appending response"); - TRITONSERVER_ErrorDelete(ce); - } - } else { - triton::common::TritonJson::Value item; - TRITONSERVER_Error* perr = - item.Parse(success_json_[i].c_str(), success_json_[i].size()); - if (perr != nullptr) { - triton::common::TritonJson::Value wrap( - root, triton::common::TritonJson::ValueType::OBJECT); - triton::common::TritonJson::Value err_part( - root, triton::common::TritonJson::ValueType::OBJECT); - TRITONSERVER_Error* ae = err_part.AddString( - "message", TRITONSERVER_ErrorMessage(perr)); - TRITONSERVER_ErrorDelete(perr); - if (ae != nullptr) { - TRITONSERVER_ErrorDelete(ae); - } - TRITONSERVER_Error* be = wrap.Add("error", std::move(err_part)); - if (be != nullptr) { - TRITONSERVER_ErrorDelete(be); - } - TRITONSERVER_Error* ce = responses.Append(std::move(wrap)); - if (ce != nullptr) { - TRITONSERVER_ErrorDelete(ce); - } - } else { - TRITONSERVER_Error* ce = responses.Append(std::move(item)); - if (ce != nullptr) { - LOG_TRITONSERVER_ERROR(ce, "multi_infer: appending response"); - TRITONSERVER_ErrorDelete(ce); - } - } - } - } - - TRITONSERVER_Error* re = root.Add("responses", std::move(responses)); - if (re != nullptr) { - LOG_TRITONSERVER_ERROR(re, "multi_infer: building root JSON"); - AddContentTypeHeader(req_, "application/json"); - EVBufferAddErrorJson(req_->buffer_out, re); - evhtp_send_reply(req_, HttpCodeFromError(re)); - TRITONSERVER_ErrorDelete(re); - evhtp_request_resume(req_); - return; - } - - triton::common::TritonJson::WriteBuffer wb; - TRITONSERVER_Error* we = root.Write(&wb); - if (we != nullptr) { - AddContentTypeHeader(req_, "application/json"); - EVBufferAddErrorJson(req_->buffer_out, we); - evhtp_send_reply(req_, HttpCodeFromError(we)); - TRITONSERVER_ErrorDelete(we); - } else { - AddContentTypeHeader(req_, "application/json"); - evbuffer_add(req_->buffer_out, wb.Base(), wb.Size()); - evhtp_send_reply(req_, EVHTP_RES_OK); - } - evhtp_request_resume(req_); - } - - evhtp_request_t* req_; - const size_t n_; - evthr_t* reply_thread_; - std::vector> irequests_; - - std::mutex mu_; - size_t done_count_{0}; - std::vector success_json_; - std::vector error_text_; - std::vector have_error_; - bool cancel_sent_{false}; - bool reply_scheduled_{false}; -}; - -class MultiInferShardRequest : public HTTPAPIServer::InferRequestClass { - public: - MultiInferShardRequest( - TRITONSERVER_Server* server, evhtp_request_t* req, - DataCompressor::Type response_compression_type, - const std::shared_ptr& triton_request, - const std::shared_ptr& shm_manager, - std::shared_ptr aggregator, const size_t slot) - : HTTPAPIServer::InferRequestClass( - server, req, response_compression_type, triton_request, shm_manager, - false /* pause */, false /* fini hook */), - aggregator_(std::move(aggregator)), slot_(slot) - { - } - - static void - InferResponseComplete( - TRITONSERVER_InferenceResponse* response, const uint32_t flags, - void* userp) - { - auto* infer_request = reinterpret_cast(userp); - - if (response != nullptr) { - ++infer_request->response_count_; - } - - TRITONSERVER_Error* err = nullptr; - evbuffer* shard_json = evbuffer_new(); - if (infer_request->response_count_ != 1) { - err = TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INTERNAL, - (std::string("expected a single response, got ") + - std::to_string(infer_request->response_count_)) - .c_str()); - } else if (response != nullptr) { - err = infer_request->FinalizeResponse(response, shard_json); -#ifdef TRITON_ENABLE_TRACING - if (infer_request->trace_ != nullptr) { - infer_request->trace_->CaptureTimestamp( - "INFER_RESPONSE_COMPLETE", TraceManager::CaptureTimestamp()); - } -#endif // TRITON_ENABLE_TRACING - } - - LOG_TRITONSERVER_ERROR( - TRITONSERVER_InferenceResponseDelete(response), - "deleting inference response"); - - std::string json_fragment; - if (err == nullptr) { - const size_t len = evbuffer_get_length(shard_json); - if (len > 0) { - const unsigned char* p = evbuffer_pullup(shard_json, -1); - if (p != nullptr) { - json_fragment.assign(reinterpret_cast(p), len); - } - } - } - evbuffer_free(shard_json); - - if (err != nullptr) { - infer_request->aggregator_->OnShardDone(infer_request->slot_, err, ""); - } else { - infer_request->aggregator_->OnShardDone( - infer_request->slot_, nullptr, json_fragment); - } - - if ((flags & TRITONSERVER_RESPONSE_COMPLETE_FINAL) == 0) { - return; - } - evthr_defer( - infer_request->thread_, DeleteMultiInferShardRequestThunk, - infer_request); - } - - private: - static void - DeleteMultiInferShardRequestThunk( - evthr_t* /*thr*/, void* arg, void* /*shared*/) - { - delete reinterpret_cast(arg); - } - - std::shared_ptr aggregator_; - const size_t slot_; -}; - -} // namespace - -void -HTTPAPIServer::HandleMultiInfer(evhtp_request_t* req) -{ - RETURN_AND_RESPOND_IF_RESTRICTED( - req, RestrictedCategory::INFERENCE, restricted_apis_); - - if (req->method != htp_method_POST) { - RETURN_AND_RESPOND_WITH_ERR( - req, EVHTP_RES_METHNALLOWED, "Method Not Allowed"); - } - - evhtp_request_pause(req); - - evbuffer* decompressed_buffer = nullptr; - TRITONSERVER_Error* derr = DecompressBuffer(req, &decompressed_buffer); - if (derr != nullptr) { - AddContentTypeHeader(req, "application/json"); - EVBufferAddErrorJson(req->buffer_out, derr); - evhtp_send_reply(req, HttpCodeFromError(derr)); - TRITONSERVER_ErrorDelete(derr); - evhtp_request_resume(req); - return; - } - - evbuffer* body_buf = - (decompressed_buffer != nullptr) ? decompressed_buffer : req->buffer_in; - - triton::common::TritonJson::Value root; - TRITONSERVER_Error* err = ParseEvbufferToTritonJson(body_buf, &root); - if (decompressed_buffer != nullptr) { - evbuffer_free(decompressed_buffer); - decompressed_buffer = nullptr; - } - if (err != nullptr) { - AddContentTypeHeader(req, "application/json"); - EVBufferAddErrorJson(req->buffer_out, err); - evhtp_send_reply(req, HttpCodeFromError(err)); - TRITONSERVER_ErrorDelete(err); - evhtp_request_resume(req); - return; - } - - triton::common::TritonJson::Value requests; - if (!root.Find("requests", &requests)) { - err = TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INVALID_ARG, - "Request body must include a JSON array field 'requests'"); - AddContentTypeHeader(req, "application/json"); - EVBufferAddErrorJson(req->buffer_out, err); - evhtp_send_reply(req, HttpCodeFromError(err)); - TRITONSERVER_ErrorDelete(err); - evhtp_request_resume(req); - return; - } - - const size_t n = requests.ArraySize(); - if (n == 0) { - err = TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INVALID_ARG, "'requests' array must be non-empty"); - AddContentTypeHeader(req, "application/json"); - EVBufferAddErrorJson(req->buffer_out, err); - evhtp_send_reply(req, HttpCodeFromError(err)); - TRITONSERVER_ErrorDelete(err); - evhtp_request_resume(req); - return; - } - if (n > kMaxMultiInferRequests) { - err = TRITONSERVER_ErrorNew( - TRITONSERVER_ERROR_INVALID_ARG, - (std::string("At most ") + std::to_string(kMaxMultiInferRequests) + - " sub-requests are allowed per multi_infer call") - .c_str()); - AddContentTypeHeader(req, "application/json"); - EVBufferAddErrorJson(req->buffer_out, err); - evhtp_send_reply(req, HttpCodeFromError(err)); - TRITONSERVER_ErrorDelete(err); - evhtp_request_resume(req); - return; - } - - struct SlotPrep { - std::string model_name; - int64_t model_version{0}; - std::string infer_body_json; - }; - std::vector slots; - slots.reserve(n); - - for (size_t i = 0; i < n; ++i) { - triton::common::TritonJson::Value slot; - err = requests.At(i, &slot); - if (err != nullptr) { - AddContentTypeHeader(req, "application/json"); - EVBufferAddErrorJson(req->buffer_out, err); - evhtp_send_reply(req, HttpCodeFromError(err)); - TRITONSERVER_ErrorDelete(err); - evhtp_request_resume(req); - return; - } - const char* mn_c; - size_t mn_len; - err = slot.MemberAsString("model_name", &mn_c, &mn_len); - if (err != nullptr) { - AddContentTypeHeader(req, "application/json"); - EVBufferAddErrorJson(req->buffer_out, err); - evhtp_send_reply(req, HttpCodeFromError(err)); - TRITONSERVER_ErrorDelete(err); - evhtp_request_resume(req); - return; - } - SlotPrep prep; - prep.model_name.assign(mn_c, mn_len); - std::string ver_str; - err = GetModelVersionStringFromSlot(slot, &ver_str); - if (err != nullptr) { - AddContentTypeHeader(req, "application/json"); - EVBufferAddErrorJson(req->buffer_out, err); - evhtp_send_reply(req, HttpCodeFromError(err)); - TRITONSERVER_ErrorDelete(err); - evhtp_request_resume(req); - return; - } - err = GetModelVersionFromString(ver_str, &prep.model_version); - if (err != nullptr) { - AddContentTypeHeader(req, "application/json"); - EVBufferAddErrorJson(req->buffer_out, err); - evhtp_send_reply(req, HttpCodeFromError(err)); - TRITONSERVER_ErrorDelete(err); - evhtp_request_resume(req); - return; - } - err = CheckTransactionPolicy(req, prep.model_name, prep.model_version); - if (err != nullptr) { - AddContentTypeHeader(req, "application/json"); - EVBufferAddErrorJson(req->buffer_out, err); - evhtp_send_reply(req, HttpCodeFromError(err)); - TRITONSERVER_ErrorDelete(err); - evhtp_request_resume(req); - return; - } - triton::common::TritonJson::Value infer_only; - err = CopyInferSlotBodyJson(slot, &infer_only); - if (err != nullptr) { - AddContentTypeHeader(req, "application/json"); - EVBufferAddErrorJson(req->buffer_out, err); - evhtp_send_reply(req, HttpCodeFromError(err)); - TRITONSERVER_ErrorDelete(err); - evhtp_request_resume(req); - return; - } - triton::common::TritonJson::WriteBuffer wb; - err = infer_only.Write(&wb); - if (err != nullptr) { - AddContentTypeHeader(req, "application/json"); - EVBufferAddErrorJson(req->buffer_out, err); - evhtp_send_reply(req, HttpCodeFromError(err)); - TRITONSERVER_ErrorDelete(err); - evhtp_request_resume(req); - return; - } - prep.infer_body_json.assign(wb.Base(), wb.Size()); - slots.push_back(std::move(prep)); - } - - evthr_t* reply_thread = evhtp_request_get_connection(req)->thread; - std::vector> irequests; - irequests.reserve(n); - for (size_t i = 0; i < n; ++i) { - TRITONSERVER_InferenceRequest* ireq = nullptr; - err = TRITONSERVER_InferenceRequestNew( - &ireq, server_.get(), slots[i].model_name.c_str(), - slots[i].model_version); - if (err != nullptr) { - for (auto& ir : irequests) { - if (ir != nullptr) { - LOG_TRITONSERVER_ERROR( - TRITONSERVER_InferenceRequestDelete(ir.get()), - "deleting unused multi_infer sub-request"); - } - } - AddContentTypeHeader(req, "application/json"); - EVBufferAddErrorJson(req->buffer_out, err); - evhtp_send_reply(req, HttpCodeFromError(err)); - TRITONSERVER_ErrorDelete(err); - evhtp_request_resume(req); - return; - } - irequests.emplace_back( - ireq, [](TRITONSERVER_InferenceRequest* r) { - LOG_TRITONSERVER_ERROR( - TRITONSERVER_InferenceRequestDelete(r), - "deleting HTTP multi_infer sub-request"); - }); - } - - std::shared_ptr aggregator = - std::make_shared(req, n, reply_thread, irequests); - - std::vector> shard_holders; - std::vector> release_holders; - shard_holders.reserve(n); - release_holders.reserve(n); - - for (size_t i = 0; i < n; ++i) { - evbuffer* body_i = evbuffer_new(); - evbuffer_add( - body_i, slots[i].infer_body_json.data(), - slots[i].infer_body_json.size()); - - const int32_t content_length = - static_cast(evbuffer_get_length(body_i)); - size_t header_length = 0; - err = GetInferenceHeaderLength(req, content_length, &header_length); - if (err != nullptr) { - aggregator->CancelAllSubRequests(); - AddContentTypeHeader(req, "application/json"); - EVBufferAddErrorJson(req->buffer_out, err); - evhtp_send_reply(req, HttpCodeFromError(err)); - TRITONSERVER_ErrorDelete(err); - evhtp_request_resume(req); - return; - } - - auto shard = std::unique_ptr( - new MultiInferShardRequest( - server_.get(), req, GetResponseCompressionType(req), - irequests[i], shm_manager_, aggregator, i)); - - err = EVRequestToTritonRequest( - req, slots[i].model_name, irequests[i].get(), body_i, shard.get(), - header_length); - if (err != nullptr) { - aggregator->CancelAllSubRequests(); - evbuffer_free(body_i); - AddContentTypeHeader(req, "application/json"); - EVBufferAddErrorJson(req->buffer_out, err); - evhtp_send_reply(req, HttpCodeFromError(err)); - TRITONSERVER_ErrorDelete(err); - evhtp_request_resume(req); - return; - } - - auto rel = std::make_unique(irequests[i], body_i); - err = ScheduleInferAsync( - req, irequests[i].get(), shard.get(), rel.get(), nullptr, - MultiInferShardRequest::InferResponseComplete); - if (err != nullptr) { - aggregator->CancelAllSubRequests(); - AddContentTypeHeader(req, "application/json"); - EVBufferAddErrorJson(req->buffer_out, err); - evhtp_send_reply(req, HttpCodeFromError(err)); - TRITONSERVER_ErrorDelete(err); - evhtp_request_resume(req); - return; - } - - shard_holders.push_back(std::move(shard)); - release_holders.push_back(std::move(rel)); - } - - for (size_t i = 0; i < n; ++i) { - release_holders[i].release(); - shard_holders[i].release(); - } -} void HTTPAPIServer::HandleInfer( evhtp_request_t* req, const std::string& model_name, @@ -5303,17 +4647,15 @@ HTTPAPIServer::GenerateRequestClass::ExactMappingOutput( return nullptr; // success } -void -HTTPAPIServer::Handle(evhtp_request_t* req) -{ - LOG_VERBOSE(1) << "HTTP request: " << req->method << " " - << req->uri->path->full; +void HTTPAPIServer::Handle(evhtp_request_t* req) { + LOG_VERBOSE(1) << "HTTP request: " << req->method << " " << req->uri->path->full; +#ifdef TRITON_ENABLE_MYSQL_ODBC if (std::string(req->uri->path->full) == "/v2/multi_infer") { HandleMultiInfer(req); return; } - +#endif if (std::string(req->uri->path->full) == "/v2/models/stats") { // model statistics HandleModelStats(req); diff --git a/src/http_server_macros.h b/src/http_server_macros.h new file mode 100644 index 0000000000..3cd47a1b14 --- /dev/null +++ b/src/http_server_macros.h @@ -0,0 +1,41 @@ +// Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// +// Shared preprocessor macros for HTTP API handlers. Macros are not C++ symbols +// and are not "in" a namespace; this header is included from within +// triton::server for consistency with http_server.cc. +// +// Prerequisites at expansion sites: HttpCodeFromError, EVBufferAddErrorJson, +// and (for RETURN_AND_RESPOND_IF_RESTRICTED) RespondIfRestricted must be +// visible — typically from the same translation unit's anonymous namespace and +// HTTPAPIServer member functions respectively. + +#pragma once + +#define RETURN_AND_RESPOND_IF_ERR(REQ, X) \ + do { \ + TRITONSERVER_Error* err__ = (X); \ + if (err__ != nullptr) { \ + EVBufferAddErrorJson((REQ)->buffer_out, err__); \ + evhtp_send_reply((REQ), HttpCodeFromError(err__)); \ + TRITONSERVER_ErrorDelete(err__); \ + return; \ + } \ + } while (false) + +#define RETURN_AND_RESPOND_WITH_ERR(REQ, CODE, MSG) \ + do { \ + EVBufferAddErrorJson((REQ)->buffer_out, MSG); \ + evhtp_send_reply((REQ), CODE); \ + return; \ + } while (false) + +#define RETURN_AND_RESPOND_IF_RESTRICTED( \ + REQ, RESTRICTED_CATEGORY, RESTRICTED_APIS) \ + do { \ + auto const& is_restricted_api = \ + RESTRICTED_APIS.IsRestricted(RESTRICTED_CATEGORY); \ + auto const& restriction = RESTRICTED_APIS.Get(RESTRICTED_CATEGORY); \ + if (is_restricted_api && RespondIfRestricted(REQ, restriction)) { \ + return; \ + } \ + } while (false) diff --git a/src/multi_infer.cc b/src/multi_infer.cc new file mode 100644 index 0000000000..e7659446e5 --- /dev/null +++ b/src/multi_infer.cc @@ -0,0 +1,635 @@ +// Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of NVIDIA CORPORATION nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +// OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "http_server.h" + +#include "classification.h" +#include "common.h" +#include "transform.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace triton { namespace server { + +#include "http_server_macros.h" + +namespace { + +constexpr size_t kMaxMultiInferRequests = 16; + +int HttpCodeFromError(TRITONSERVER_Error* error) { + if (error == nullptr) { + return EVHTP_RES_OK; + } + switch (TRITONSERVER_ErrorCode(error)) { + case TRITONSERVER_ERROR_INTERNAL: + return EVHTP_RES_SERVERR; + case TRITONSERVER_ERROR_NOT_FOUND: + return EVHTP_RES_NOTFOUND; + case TRITONSERVER_ERROR_UNAVAILABLE: + return EVHTP_RES_SERVUNAVAIL; + case TRITONSERVER_ERROR_UNSUPPORTED: + return EVHTP_RES_NOTIMPL; + case TRITONSERVER_ERROR_UNKNOWN: + case TRITONSERVER_ERROR_INVALID_ARG: + case TRITONSERVER_ERROR_ALREADY_EXISTS: + case TRITONSERVER_ERROR_CANCELLED: + return EVHTP_RES_BADREQ; + } + + return EVHTP_RES_BADREQ; +} + +void EVBufferAddErrorJson(evbuffer* buffer, const char* message) { + triton::common::TritonJson::Value response(triton::common::TritonJson::ValueType::OBJECT); + response.AddStringRef("error", message, strlen(message)); + + triton::common::TritonJson::WriteBuffer buffer_json; + response.Write(&buffer_json); + + evbuffer_add(buffer, buffer_json.Base(), buffer_json.Size()); +} + +void EVBufferAddErrorJson(evbuffer* buffer, TRITONSERVER_Error* err) { + const char* message = TRITONSERVER_ErrorMessage(err); + EVBufferAddErrorJson(buffer, message); +} + +void AddContentTypeHeader(evhtp_request_t* req, const char* type) { + auto content_header = evhtp_headers_find_header(req->headers_out, kContentTypeHeader); + if (content_header) { + evhtp_header_rm_and_free(req->headers_out, content_header); + } + + evhtp_headers_add_header(req->headers_out, evhtp_header_new(kContentTypeHeader, type, 1, 1)); +} + +TRITONSERVER_Error* CopyInferSlotBodyJson(triton::common::TritonJson::Value& slot, triton::common::TritonJson::Value* infer_json) { + *infer_json = triton::common::TritonJson::Value(triton::common::TritonJson::ValueType::OBJECT); + { + triton::common::TritonJson::Value v; + if (slot.Find("id", &v)) { + RETURN_IF_ERR(infer_json->Add("id", std::move(v))); + } + } + { + triton::common::TritonJson::Value v; + if (!slot.Find("inputs", &v)) { + return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, "Each entry in 'requests' must contain an 'inputs' array"); + } + RETURN_IF_ERR(infer_json->Add("inputs", std::move(v))); + } + { + triton::common::TritonJson::Value v; + if (slot.Find("outputs", &v)) { + RETURN_IF_ERR(infer_json->Add("outputs", std::move(v))); + } + } + { + triton::common::TritonJson::Value v; + if (slot.Find("parameters", &v)) { + RETURN_IF_ERR(infer_json->Add("parameters", std::move(v))); + } + } + return nullptr; +} + +TRITONSERVER_Error* GetModelVersionStringFromSlot(triton::common::TritonJson::Value& slot, std::string* ver_out) +{ + ver_out->clear(); + triton::common::TritonJson::Value mv; + if (!slot.Find("model_version", &mv)) { + return nullptr; + } + if (mv.IsString()) { + const char* s; + size_t len; + RETURN_IF_ERR(mv.AsString(&s, &len)); + ver_out->assign(s, len); + return nullptr; + } + if (mv.IsNumber()) { + int64_t iv; + RETURN_IF_ERR(mv.AsInt(&iv)); + *ver_out = std::to_string(iv); + return nullptr; + } + return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, "'model_version' must be a string or integer"); +} + +class MultiInferAggregator : public std::enable_shared_from_this { + private: + struct FinishPayload { + std::shared_ptr agg; + }; + + public: + MultiInferAggregator(evhtp_request_t* req, size_t slot_count, evthr_t* reply_thread, std::vector> irequests) + : req_(req), n_(slot_count), reply_thread_(reply_thread), + irequests_(std::move(irequests)), success_json_(slot_count), + error_text_(slot_count), have_error_(slot_count, 0){} + + std::shared_ptr IrequestAt(size_t i) const + { + return irequests_[i]; + } + + void CancelAllSubRequests() + { + std::lock_guard lk(mu_); + if (cancel_sent_) { + return; + } + cancel_sent_ = true; + for (auto& ir : irequests_) { + if (ir != nullptr) { + LOG_TRITONSERVER_ERROR(TRITONSERVER_InferenceRequestCancel(ir.get()), "cancelling multi_infer sub-request"); + } + } + } + + void OnShardDone(size_t slot, TRITONSERVER_Error* finalize_err, const std::string& response_json) { + std::shared_ptr self; + { + std::lock_guard lk(mu_); + if (finalize_err != nullptr) { + have_error_[slot] = 1; + error_text_[slot] = TRITONSERVER_ErrorMessage(finalize_err); + TRITONSERVER_ErrorDelete(finalize_err); + if (!cancel_sent_) { + cancel_sent_ = true; + for (auto& ir : irequests_) { + if (ir != nullptr) { + LOG_TRITONSERVER_ERROR(TRITONSERVER_InferenceRequestCancel(ir.get()), "cancelling multi_infer sub-request"); + } + } + } + } else { + success_json_[slot] = response_json; + } + done_count_++; + if (done_count_ < n_ || reply_scheduled_) { + return; + } + reply_scheduled_ = true; + self = shared_from_this(); + } + + auto* fp = new FinishPayload{std::move(self)}; + evthr_defer(reply_thread_, FinishThunk, fp); + } + + private: + static void FinishThunk(evthr_t* /*thr*/, void* arg, void* /*shared*/) { + std::unique_ptr fp(static_cast(arg)); + fp->agg->WriteHttpReply(); + } + + void WriteHttpReply() { + triton::common::TritonJson::Value root(triton::common::TritonJson::ValueType::OBJECT); + triton::common::TritonJson::Value responses(root, triton::common::TritonJson::ValueType::ARRAY); + for (size_t i = 0; i < n_; ++i) { + if (have_error_[i]) { + triton::common::TritonJson::Value item(root, triton::common::TritonJson::ValueType::OBJECT); + triton::common::TritonJson::Value err_part(root, triton::common::TritonJson::ValueType::OBJECT); + TRITONSERVER_Error* ae = err_part.AddString("message", error_text_[i]); + if (ae != nullptr) { + LOG_TRITONSERVER_ERROR(ae, "multi_infer: building error JSON"); + TRITONSERVER_ErrorDelete(ae); + } + TRITONSERVER_Error* be = item.Add("error", std::move(err_part)); + if (be != nullptr) { + LOG_TRITONSERVER_ERROR(be, "multi_infer: building error JSON"); + TRITONSERVER_ErrorDelete(be); + } + TRITONSERVER_Error* ce = responses.Append(std::move(item)); + if (ce != nullptr) { + LOG_TRITONSERVER_ERROR(ce, "multi_infer: appending response"); + TRITONSERVER_ErrorDelete(ce); + } + } + else { + triton::common::TritonJson::Value item; + TRITONSERVER_Error* perr = item.Parse(success_json_[i].c_str(), success_json_[i].size()); + if (perr != nullptr) { + triton::common::TritonJson::Value wrap(root, triton::common::TritonJson::ValueType::OBJECT); + triton::common::TritonJson::Value err_part(root, triton::common::TritonJson::ValueType::OBJECT); + TRITONSERVER_Error* ae = err_part.AddString("message", TRITONSERVER_ErrorMessage(perr)); + TRITONSERVER_ErrorDelete(perr); + if (ae != nullptr) { + TRITONSERVER_ErrorDelete(ae); + } + TRITONSERVER_Error* be = wrap.Add("error", std::move(err_part)); + if (be != nullptr) { + TRITONSERVER_ErrorDelete(be); + } + TRITONSERVER_Error* ce = responses.Append(std::move(wrap)); + if (ce != nullptr) { + TRITONSERVER_ErrorDelete(ce); + } + } else { + TRITONSERVER_Error* ce = responses.Append(std::move(item)); + if (ce != nullptr) { + LOG_TRITONSERVER_ERROR(ce, "multi_infer: appending response"); + TRITONSERVER_ErrorDelete(ce); + } + } + } + } + + TRITONSERVER_Error* re = root.Add("responses", std::move(responses)); + if (re != nullptr) { + LOG_TRITONSERVER_ERROR(re, "multi_infer: building root JSON"); + AddContentTypeHeader(req_, "application/json"); + EVBufferAddErrorJson(req_->buffer_out, re); + evhtp_send_reply(req_, HttpCodeFromError(re)); + TRITONSERVER_ErrorDelete(re); + evhtp_request_resume(req_); + return; + } + + triton::common::TritonJson::WriteBuffer wb; + TRITONSERVER_Error* we = root.Write(&wb); + if (we != nullptr) { + AddContentTypeHeader(req_, "application/json"); + EVBufferAddErrorJson(req_->buffer_out, we); + evhtp_send_reply(req_, HttpCodeFromError(we)); + TRITONSERVER_ErrorDelete(we); + } + else { + AddContentTypeHeader(req_, "application/json"); + evbuffer_add(req_->buffer_out, wb.Base(), wb.Size()); + evhtp_send_reply(req_, EVHTP_RES_OK); + } + evhtp_request_resume(req_); + } + + evhtp_request_t* req_; + const size_t n_; + evthr_t* reply_thread_; + std::vector> irequests_; + + std::mutex mu_; + size_t done_count_{0}; + std::vector success_json_; + std::vector error_text_; + std::vector have_error_; + bool cancel_sent_{false}; + bool reply_scheduled_{false}; +}; + +class MultiInferShardRequest : public HTTPAPIServer::InferRequestClass { + public: + MultiInferShardRequest(TRITONSERVER_Server* server, evhtp_request_t* req, + DataCompressor::Type response_compression_type, + const std::shared_ptr& triton_request, + const std::shared_ptr& shm_manager, + std::shared_ptr aggregator, const size_t slot) + : HTTPAPIServer::InferRequestClass(server, req, response_compression_type, triton_request, shm_manager, false /* pause */, false /* fini hook */), aggregator_(std::move(aggregator)), slot_(slot){} + + static void InferResponseComplete(TRITONSERVER_InferenceResponse* response, const uint32_t flags, void* userp) { + auto* infer_request = reinterpret_cast(userp); + + if (response != nullptr) { + ++infer_request->response_count_; + } + + TRITONSERVER_Error* err = nullptr; + evbuffer* shard_json = evbuffer_new(); + if (infer_request->response_count_ != 1) { + const std::string msg = std::string("expected a single response, got ") + std::to_string(infer_request->response_count_); + err = TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INTERNAL, msg.c_str()); + } else if (response != nullptr) { + err = infer_request->FinalizeResponse(response, shard_json); +#ifdef TRITON_ENABLE_TRACING + if (infer_request->trace_ != nullptr) { + infer_request->trace_->CaptureTimestamp("INFER_RESPONSE_COMPLETE", TraceManager::CaptureTimestamp()); + } +#endif // TRITON_ENABLE_TRACING + } + + LOG_TRITONSERVER_ERROR(TRITONSERVER_InferenceResponseDelete(response), "deleting inference response"); + + std::string json_fragment; + if (err == nullptr) { + const size_t len = evbuffer_get_length(shard_json); + if (len > 0) { + const unsigned char* p = evbuffer_pullup(shard_json, -1); + if (p != nullptr) { + json_fragment.assign(reinterpret_cast(p), len); + } + } + } + evbuffer_free(shard_json); + + if (err != nullptr) { + infer_request->aggregator_->OnShardDone(infer_request->slot_, err, ""); + } + else { + infer_request->aggregator_->OnShardDone(infer_request->slot_, nullptr, json_fragment); + } + + if ((flags & TRITONSERVER_RESPONSE_COMPLETE_FINAL) == 0) { + return; + } + evthr_defer(infer_request->thread_, DeleteMultiInferShardRequestThunk, infer_request); + } + + private: + static void DeleteMultiInferShardRequestThunk(evthr_t* /*thr*/, void* arg, void* /*shared*/) { + delete reinterpret_cast(arg); + } + + std::shared_ptr aggregator_; + const size_t slot_; +}; + +} // namespace + +void HTTPAPIServer::HandleMultiInfer(evhtp_request_t* req) { + RETURN_AND_RESPOND_IF_RESTRICTED(req, RestrictedCategory::INFERENCE, restricted_apis_); + + if (req->method != htp_method_POST) { + RETURN_AND_RESPOND_WITH_ERR(req, EVHTP_RES_METHNALLOWED, "Method Not Allowed"); + } + + evhtp_request_pause(req); + + evbuffer* decompressed_buffer = nullptr; + TRITONSERVER_Error* derr = DecompressBuffer(req, &decompressed_buffer); + if (derr != nullptr) { + AddContentTypeHeader(req, "application/json"); + EVBufferAddErrorJson(req->buffer_out, derr); + evhtp_send_reply(req, HttpCodeFromError(derr)); + TRITONSERVER_ErrorDelete(derr); + evhtp_request_resume(req); + return; + } + + evbuffer* body_buf = (decompressed_buffer != nullptr) ? decompressed_buffer : req->buffer_in; + + triton::common::TritonJson::Value root; + TRITONSERVER_Error* err = nullptr; + const size_t body_len = evbuffer_get_length(body_buf); + std::vector body_copy(body_len); + if (body_len > 0) { + const ssize_t nread = evbuffer_copyout(body_buf, body_copy.data(), body_len); + if (nread < 0 || static_cast(nread) != body_len) { + err = TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INTERNAL, "failed to read multi_infer request body"); + } + } + if (err == nullptr) { + const std::string body_json(body_copy.data(), body_len); + rapidjson::Document parsed; + err = triton::server::ParseRequest(body_json, server_.get(), &parsed); + if (err == nullptr) { + rapidjson::StringBuffer sb; + rapidjson::Writer writer(sb); + parsed.Accept(writer); + const std::string transformed(sb.GetString(), sb.GetSize()); + err = root.Parse(transformed.c_str(), transformed.size()); + } + } + if (decompressed_buffer != nullptr) { + evbuffer_free(decompressed_buffer); + decompressed_buffer = nullptr; + } + if (err != nullptr) { + AddContentTypeHeader(req, "application/json"); + EVBufferAddErrorJson(req->buffer_out, err); + evhtp_send_reply(req, HttpCodeFromError(err)); + TRITONSERVER_ErrorDelete(err); + evhtp_request_resume(req); + return; + } + + triton::common::TritonJson::Value requests; + if (!root.Find("requests", &requests)) { + err = TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, "Request body must include a JSON array field 'requests'"); + AddContentTypeHeader(req, "application/json"); + EVBufferAddErrorJson(req->buffer_out, err); + evhtp_send_reply(req, HttpCodeFromError(err)); + TRITONSERVER_ErrorDelete(err); + evhtp_request_resume(req); + return; + } + + const size_t n = requests.ArraySize(); + if (n == 0) { + err = TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, "'requests' array must be non-empty"); + AddContentTypeHeader(req, "application/json"); + EVBufferAddErrorJson(req->buffer_out, err); + evhtp_send_reply(req, HttpCodeFromError(err)); + TRITONSERVER_ErrorDelete(err); + evhtp_request_resume(req); + return; + } + if (n > kMaxMultiInferRequests) { + const std::string lim = "At most " + std::to_string(kMaxMultiInferRequests) + " sub-requests are allowed per multi_infer call"; + err = TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, lim.c_str()); + AddContentTypeHeader(req, "application/json"); + EVBufferAddErrorJson(req->buffer_out, err); + evhtp_send_reply(req, HttpCodeFromError(err)); + TRITONSERVER_ErrorDelete(err); + evhtp_request_resume(req); + return; + } + + struct SlotPrep { + std::string model_name; + int64_t model_version{0}; + std::string infer_body_json; + }; + std::vector slots; + slots.reserve(n); + + for (size_t i = 0; i < n; ++i) { + triton::common::TritonJson::Value slot; + err = requests.At(i, &slot); + if (err != nullptr) { + AddContentTypeHeader(req, "application/json"); + EVBufferAddErrorJson(req->buffer_out, err); + evhtp_send_reply(req, HttpCodeFromError(err)); + TRITONSERVER_ErrorDelete(err); + evhtp_request_resume(req); + return; + } + const char* mn_c; + size_t mn_len; + err = slot.MemberAsString("model_name", &mn_c, &mn_len); + if (err != nullptr) { + AddContentTypeHeader(req, "application/json"); + EVBufferAddErrorJson(req->buffer_out, err); + evhtp_send_reply(req, HttpCodeFromError(err)); + TRITONSERVER_ErrorDelete(err); + evhtp_request_resume(req); + return; + } + SlotPrep prep; + prep.model_name.assign(mn_c, mn_len); + std::string ver_str; + err = GetModelVersionStringFromSlot(slot, &ver_str); + if (err != nullptr) { + AddContentTypeHeader(req, "application/json"); + EVBufferAddErrorJson(req->buffer_out, err); + evhtp_send_reply(req, HttpCodeFromError(err)); + TRITONSERVER_ErrorDelete(err); + evhtp_request_resume(req); + return; + } + err = GetModelVersionFromString(ver_str, &prep.model_version); + if (err != nullptr) { + AddContentTypeHeader(req, "application/json"); + EVBufferAddErrorJson(req->buffer_out, err); + evhtp_send_reply(req, HttpCodeFromError(err)); + TRITONSERVER_ErrorDelete(err); + evhtp_request_resume(req); + return; + } + err = CheckTransactionPolicy(req, prep.model_name, prep.model_version); + if (err != nullptr) { + AddContentTypeHeader(req, "application/json"); + EVBufferAddErrorJson(req->buffer_out, err); + evhtp_send_reply(req, HttpCodeFromError(err)); + TRITONSERVER_ErrorDelete(err); + evhtp_request_resume(req); + return; + } + triton::common::TritonJson::Value infer_only; + err = CopyInferSlotBodyJson(slot, &infer_only); + if (err != nullptr) { + AddContentTypeHeader(req, "application/json"); + EVBufferAddErrorJson(req->buffer_out, err); + evhtp_send_reply(req, HttpCodeFromError(err)); + TRITONSERVER_ErrorDelete(err); + evhtp_request_resume(req); + return; + } + triton::common::TritonJson::WriteBuffer wb; + err = infer_only.Write(&wb); + if (err != nullptr) { + AddContentTypeHeader(req, "application/json"); + EVBufferAddErrorJson(req->buffer_out, err); + evhtp_send_reply(req, HttpCodeFromError(err)); + TRITONSERVER_ErrorDelete(err); + evhtp_request_resume(req); + return; + } + prep.infer_body_json.assign(wb.Base(), wb.Size()); + slots.push_back(std::move(prep)); + } + + evthr_t* reply_thread = evhtp_request_get_connection(req)->thread; + std::vector> irequests; + irequests.reserve(n); + for (size_t i = 0; i < n; ++i) { + TRITONSERVER_InferenceRequest* ireq = nullptr; + err = TRITONSERVER_InferenceRequestNew(&ireq, server_.get(), slots[i].model_name.c_str(), slots[i].model_version); + if (err != nullptr) { + for (auto& ir : irequests) { + if (ir != nullptr) { + LOG_TRITONSERVER_ERROR(TRITONSERVER_InferenceRequestDelete(ir.get()), "deleting unused multi_infer sub-request"); + } + } + AddContentTypeHeader(req, "application/json"); + EVBufferAddErrorJson(req->buffer_out, err); + evhtp_send_reply(req, HttpCodeFromError(err)); + TRITONSERVER_ErrorDelete(err); + evhtp_request_resume(req); + return; + } + irequests.emplace_back(ireq, [](TRITONSERVER_InferenceRequest* r) { + LOG_TRITONSERVER_ERROR(TRITONSERVER_InferenceRequestDelete(r),"deleting HTTP multi_infer sub-request"); + }); + } + + std::shared_ptr aggregator = std::make_shared(req, n, reply_thread, irequests); + std::vector> shard_holders; + std::vector> release_holders; + shard_holders.reserve(n); + release_holders.reserve(n); + + for (size_t i = 0; i < n; ++i) { + evbuffer* body_i = evbuffer_new(); + evbuffer_add(body_i, slots[i].infer_body_json.data(), slots[i].infer_body_json.size()); + const int32_t content_length = static_cast(evbuffer_get_length(body_i)); + size_t header_length = 0; + err = GetInferenceHeaderLength(req, content_length, &header_length); + if (err != nullptr) { + aggregator->CancelAllSubRequests(); + AddContentTypeHeader(req, "application/json"); + EVBufferAddErrorJson(req->buffer_out, err); + evhtp_send_reply(req, HttpCodeFromError(err)); + TRITONSERVER_ErrorDelete(err); + evhtp_request_resume(req); + return; + } + + auto shard = std::make_unique(server_.get(), req, GetResponseCompressionType(req), irequests[i], shm_manager_, aggregator, i); + + err = EVRequestToTritonRequest(req, slots[i].model_name, irequests[i].get(), body_i, shard.get(), header_length); + if (err != nullptr) { + aggregator->CancelAllSubRequests(); + evbuffer_free(body_i); + AddContentTypeHeader(req, "application/json"); + EVBufferAddErrorJson(req->buffer_out, err); + evhtp_send_reply(req, HttpCodeFromError(err)); + TRITONSERVER_ErrorDelete(err); + evhtp_request_resume(req); + return; + } + + auto rel = std::make_unique(irequests[i], body_i); + err = ScheduleInferAsync(req, irequests[i].get(), shard.get(), rel.get(), nullptr, MultiInferShardRequest::InferResponseComplete); + if (err != nullptr) { + aggregator->CancelAllSubRequests(); + AddContentTypeHeader(req, "application/json"); + EVBufferAddErrorJson(req->buffer_out, err); + evhtp_send_reply(req, HttpCodeFromError(err)); + TRITONSERVER_ErrorDelete(err); + evhtp_request_resume(req); + return; + } + + shard_holders.push_back(std::move(shard)); + release_holders.push_back(std::move(rel)); + } + + for (size_t i = 0; i < n; ++i) { + release_holders[i].release(); + shard_holders[i].release(); + } +} + +}} // namespace triton::server diff --git a/src/transform.cc b/src/transform.cc new file mode 100644 index 0000000000..b6e3a14e60 --- /dev/null +++ b/src/transform.cc @@ -0,0 +1,343 @@ +// Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of NVIDIA CORPORATION nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +// OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#ifdef TRITON_ENABLE_MYSQL_ODBC +#include "transform.h" +#include "mysql_odbc_connection_pool.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +int FeatureIdxFromJsonValue(const char* feature_name, const rapidjson::Value& v, const triton::server::FeatureMappingTables* tables) { + if (tables == nullptr) { + return -1; + } + if (v.IsString()) { + return triton::server::GetFeatureMappingIdx(feature_name, v.GetString(), tables); + } + if (v.IsInt()) { + const std::string tmp = std::to_string(v.GetInt()); + return triton::server::GetFeatureMappingIdx(feature_name, tmp.c_str(), tables); + } + if (v.IsUint()) { + const std::string tmp = std::to_string(v.GetUint()); + return triton::server::GetFeatureMappingIdx(feature_name, tmp.c_str(), tables); + } + if (v.IsInt64()) { + const std::string tmp = std::to_string(v.GetInt64()); + return triton::server::GetFeatureMappingIdx(feature_name, tmp.c_str(), tables); + } + if (v.IsUint64()) { + const std::string tmp = std::to_string(v.GetUint64()); + return triton::server::GetFeatureMappingIdx(feature_name, tmp.c_str(), tables); + } + return -1; +} +} // namespace + +namespace triton { namespace server { + +TRITONSERVER_Error* ParseRequest(const std::string& json, TRITONSERVER_Server* server, rapidjson::Document* out_doc) { + if (out_doc == nullptr) { + return TRITONSERVER_ErrorNew( + TRITONSERVER_ERROR_INVALID_ARG, "output document pointer is null"); + } + + rapidjson::Document doc; + doc.Parse(json.data(), json.size()); + if (doc.HasParseError()) { + const char* msg = rapidjson::GetParseError_En(doc.GetParseError()); + return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, msg); + } + + if (server != nullptr && doc.IsObject() && doc.HasMember("imps") && doc["imps"].IsArray()) { + return GenerateInputVectors(doc, server, out_doc); + } + + *out_doc = std::move(doc); + return nullptr; +} + +TRITONSERVER_Error* GetReadyModelNames(TRITONSERVER_Server* server, std::vector* out) { + if (out == nullptr) { + return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, "output vector pointer is null"); + } + out->clear(); + + TRITONSERVER_Message* message = nullptr; + TRITONSERVER_Error* err = TRITONSERVER_ServerModelIndex(server, TRITONSERVER_INDEX_FLAG_READY, &message); + if (err != nullptr) { + return err; + } + + const char* buffer = nullptr; + size_t byte_size = 0; + err = TRITONSERVER_MessageSerializeToJson(message, &buffer, &byte_size); + if (err != nullptr) { + TRITONSERVER_MessageDelete(message); + return err; + } + const std::string index_json(buffer, byte_size); + TRITONSERVER_MessageDelete(message); + + rapidjson::Document index_doc; + index_doc.Parse(index_json.data(), index_json.size()); + if (index_doc.HasParseError()) { + const std::string parse_err = std::string("failed to parse model index JSON: ") + rapidjson::GetParseError_En(index_doc.GetParseError()) + " at offset " + std::to_string(index_doc.GetErrorOffset()); + return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INTERNAL, parse_err.c_str()); + } + if (!index_doc.IsArray()) { + return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INTERNAL, "model index JSON root is not an array"); + } + + for (rapidjson::SizeType i = 0; i < index_doc.Size(); ++i) { + const rapidjson::Value& o = index_doc[i]; + if (!o.IsObject() || !o.HasMember("name")) { + continue; + } + const rapidjson::Value& n = o["name"]; + if (!n.IsString()) { + continue; + } + out->emplace_back(n.GetString()); + } + + return nullptr; +} + +TRITONSERVER_Error* AppendRowToNamedDoubleBuffers(NamedDoubleBuffers* buffers, ModelNameToFeatureCount* feature_counts, + const std::string& vector_name, const std::vector& row, size_t feature_count) { + if (buffers == nullptr || feature_counts == nullptr) { + return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, "buffer pointers are null"); + } + if (row.size() != feature_count || feature_count == 0) { + return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, "row width does not match feature_count"); + } + + auto fc_it = feature_counts->find(vector_name); + if (fc_it == feature_counts->end()) { + (*feature_counts)[vector_name] = feature_count; + } else if (fc_it->second != feature_count) { + return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, "inconsistent feature_count for model buffer"); + } + + std::vector& buf = (*buffers)[vector_name]; + buf.insert(buf.end(), row.begin(), row.end()); + return nullptr; +} + +TRITONSERVER_Error* BuildMultiInferRequestDocument(const NamedDoubleBuffers& buffers, const ModelNameToFeatureCount& feature_counts, rapidjson::Document* out_doc) { + if (out_doc == nullptr) { + return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, "output document pointer is null"); + } + + std::vector names; + names.reserve(buffers.size()); + for (const auto& kv : buffers) { + names.push_back(kv.first); + } + std::sort(names.begin(), names.end()); + + rapidjson::Document doc(rapidjson::kObjectType); + auto& alloc = doc.GetAllocator(); + rapidjson::Value requests(rapidjson::kArrayType); + + for (const std::string& model_name : names) { + auto bc_it = buffers.find(model_name); + auto fc_it = feature_counts.find(model_name); + if (bc_it == buffers.end() || fc_it == feature_counts.end()) { + return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INTERNAL, "internal buffer map inconsistency"); + } + const size_t feature_count = fc_it->second; + if (feature_count == 0) { + return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, "zero feature_count for model"); + } + const std::vector& flat = bc_it->second; + if (flat.size() % feature_count != 0 || flat.empty()) { + return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, "buffer length is not a multiple of feature_count"); + } + const size_t rows = flat.size() / feature_count; + + rapidjson::Value req(rapidjson::kObjectType); + req.AddMember("model_name", rapidjson::Value(model_name.c_str(), static_cast(model_name.size()), alloc).Move(), alloc); + + rapidjson::Value data(rapidjson::kArrayType); + data.Reserve(static_cast(flat.size()), alloc); + for (double v : flat) { + data.PushBack(v, alloc); + } + + rapidjson::Value shape(rapidjson::kArrayType); + shape.PushBack(static_cast(rows), alloc); + shape.PushBack(static_cast(feature_count), alloc); + + rapidjson::Value input0(rapidjson::kObjectType); + input0.AddMember("name", "input__0", alloc); + input0.AddMember("datatype", "FP32", alloc); + input0.AddMember("shape", shape, alloc); + input0.AddMember("data", data, alloc); + + rapidjson::Value inputs(rapidjson::kArrayType); + inputs.PushBack(input0, alloc); + + rapidjson::Value outputs(rapidjson::kArrayType); + rapidjson::Value out0(rapidjson::kObjectType); + out0.AddMember("name", "output__0", alloc); + outputs.PushBack(out0, alloc); + + req.AddMember("inputs", inputs, alloc); + req.AddMember("outputs", outputs, alloc); + requests.PushBack(req, alloc); + } + + doc.AddMember("requests", requests, alloc); + *out_doc = std::move(doc); + return nullptr; +} + +TRITONSERVER_Error* GenerateInputVectors(const rapidjson::Document& doc, TRITONSERVER_Server* server, rapidjson::Document* out_doc) { + if (out_doc == nullptr || server == nullptr) { + return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, "invalid argument"); + } + if (!doc.IsObject() || !doc.HasMember("imps") || !doc["imps"].IsArray()) { + return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, "expected object with imps array"); + } + + const CampaignToFeatureMappings* cmap = ActiveCampaignToFeatureMappings(); + if (cmap == nullptr || cmap->empty()) { + return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_UNAVAILABLE, "campaign feature mappings are not loaded"); + } + + std::vector ready_model_names; + TRITONSERVER_Error* err = GetReadyModelNames(server, &ready_model_names); + if (err != nullptr) { + return err; + } + if (ready_model_names.empty()) { + return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, "no ready models reported by server"); + } + + NamedDoubleBuffers buffers; + ModelNameToFeatureCount counts; + + const rapidjson::Value& imps = doc["imps"]; + for (rapidjson::SizeType ii = 0; ii < imps.Size(); ++ii) { + const rapidjson::Value& imp = imps[ii]; + if (!imp.IsObject()) { + continue; + } + if (!imp.HasMember("camps") || !imp["camps"].IsArray() || imp["camps"].Size() == 0) { + return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, "each impression must include a non-empty camps array"); + } + const rapidjson::Value& camps = imp["camps"]; + for (rapidjson::SizeType ci = 0; ci < camps.Size(); ++ci) { + const rapidjson::Value& camp = camps[ci]; + if (!camp.IsObject() || !camp.HasMember("cid") || !camp["cid"].IsInt()) { + return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, "each camp must be an object with integer 'cid'"); + } + const int32_t campaign_id = camp["cid"].GetInt(); + + auto cmap_it = cmap->find(campaign_id); + if(cmap_it == cmap->end()) { + cmap_it = cmap->find(0); + } + if (cmap_it == cmap->end()) { + const std::string unknown_campaign = std::string("unknown campaign_id ") + std::to_string(campaign_id); + return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, unknown_campaign.c_str()); + } + const std::string& model_name = cmap_it->second.model_name; + const std::vector& feature_sequence = cmap_it->second.feature_sequence; + const FeatureMappingTables& tables = cmap_it->second.feature_mapping; + + if (std::find(ready_model_names.begin(), ready_model_names.end(), model_name) == ready_model_names.end()) { + const std::string not_ready = "model " + model_name + " not ready"; + return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, not_ready.c_str()); + } + + std::vector row(feature_sequence.size(), 0.0); + int adsize_idx = -1; + + for (size_t fi = 0; fi < feature_sequence.size(); ++fi) { + const std::string& feature = feature_sequence[fi]; + const char* fkey = feature.c_str(); + + if (feature == "adsize") { + adsize_idx = static_cast(fi); + continue; + } + + const rapidjson::Value* src = nullptr; + if (feature == "cookie" || feature == "rnk") { + if (camp.IsObject() && camp.HasMember(fkey)) { + src = &camp[fkey]; + } + } + else if(feature == "campid") { + if (camp.IsObject() && camp.HasMember("cid")) { + src = &camp["cid"]; + } + } + else { + if (imp.IsObject() && imp.HasMember(fkey)) { + src = &imp[fkey]; + } + } + + if (src == nullptr) { + const std::string missing_field = std::string("missing JSON field for feature '") + feature + "'"; + return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, missing_field.c_str()); + } + int idx = FeatureIdxFromJsonValue(feature.c_str(), *src, &tables); + row[fi] = static_cast(idx); + } + + if (adsize_idx >= 0 && camp.HasMember("adsize") && camp["adsize"].IsArray()) { + const rapidjson::Value& adsize = camp["adsize"]; + for (rapidjson::SizeType ai = 0; ai < adsize.Size(); ++ai) { + const rapidjson::Value& adsize_item = adsize[ai]; + int mapped = FeatureIdxFromJsonValue("adsize", adsize_item, &tables); + row[static_cast(adsize_idx)] = static_cast(mapped); + err = AppendRowToNamedDoubleBuffers(&buffers, &counts, model_name, row, feature_sequence.size()); + if (err != nullptr) { + return err; + } + } + } + } + } + err = BuildMultiInferRequestDocument(buffers, counts, out_doc); + return err; + } +} +} // namespace triton::server +#endif // TRITON_ENABLE_MYSQL_ODBC diff --git a/src/transform.h b/src/transform.h new file mode 100644 index 0000000000..a70540b3e8 --- /dev/null +++ b/src/transform.h @@ -0,0 +1,71 @@ +// Copyright 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of NVIDIA CORPORATION nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +// OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#pragma once + +#include +#include +#include +#include + +#include "triton/core/tritonserver.h" + +namespace triton { namespace server { + +// Parses `json` into a document. If built with `TRITON_ENABLE_MYSQL_ODBC` and +// `server` is non-null and the root object contains `"imps"`, replaces +// `*out_doc` with a multi-infer request built from impressions; otherwise +// stores the parsed root in `*out_doc`. +// Returns nullptr on success, or a TRITONSERVER_Error that the caller must +// delete with TRITONSERVER_ErrorDelete on failure. +TRITONSERVER_Error* ParseRequest( + const std::string& json, TRITONSERVER_Server* server, + rapidjson::Document* out_doc); + +#ifdef TRITON_ENABLE_MYSQL_ODBC + +// Builds `*out_doc` as a `POST /v2/multi_infer` body from `doc` (imps/camps). +TRITONSERVER_Error* GenerateInputVectors( + const rapidjson::Document& doc, TRITONSERVER_Server* server, + rapidjson::Document* out_doc); + +TRITONSERVER_Error* GetReadyModelNames( + TRITONSERVER_Server* server, std::vector* out); + +using NamedDoubleBuffers = std::unordered_map>; +using ModelNameToFeatureCount = std::unordered_map; + +TRITONSERVER_Error* AppendRowToNamedDoubleBuffers( + NamedDoubleBuffers* buffers, ModelNameToFeatureCount* feature_counts, + const std::string& vector_name, const std::vector& row, + size_t feature_count); + +TRITONSERVER_Error* BuildMultiInferRequestDocument( + const NamedDoubleBuffers& buffers, + const ModelNameToFeatureCount& feature_counts, rapidjson::Document* out_doc); + +#endif // TRITON_ENABLE_MYSQL_ODBC + +}} // namespace triton::server From 724577a8260d53692d92d4aacdcbc96901bbbc08 Mon Sep 17 00:00:00 2001 From: "shantanu.mirajgave" Date: Tue, 16 Jun 2026 00:22:07 -0700 Subject: [PATCH 230/230] Converting vector to unordered set --- src/transform.cc | 56 +++++++++++++++++++++++++++--------------------- src/transform.h | 7 +++--- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/transform.cc b/src/transform.cc index b6e3a14e60..eb2f46e0f7 100644 --- a/src/transform.cc +++ b/src/transform.cc @@ -30,7 +30,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -45,23 +47,23 @@ int FeatureIdxFromJsonValue(const char* feature_name, const rapidjson::Value& v, if (v.IsString()) { return triton::server::GetFeatureMappingIdx(feature_name, v.GetString(), tables); } + char num_buf[48]; + int n = 0; if (v.IsInt()) { - const std::string tmp = std::to_string(v.GetInt()); - return triton::server::GetFeatureMappingIdx(feature_name, tmp.c_str(), tables); - } - if (v.IsUint()) { - const std::string tmp = std::to_string(v.GetUint()); - return triton::server::GetFeatureMappingIdx(feature_name, tmp.c_str(), tables); - } - if (v.IsInt64()) { - const std::string tmp = std::to_string(v.GetInt64()); - return triton::server::GetFeatureMappingIdx(feature_name, tmp.c_str(), tables); + n = std::snprintf(num_buf, sizeof(num_buf), "%d", v.GetInt()); + } else if (v.IsUint()) { + n = std::snprintf(num_buf, sizeof(num_buf), "%u", v.GetUint()); + } else if (v.IsInt64()) { + n = std::snprintf(num_buf, sizeof(num_buf), "%lld", static_cast(v.GetInt64())); + } else if (v.IsUint64()) { + n = std::snprintf(num_buf, sizeof(num_buf), "%llu", static_cast(v.GetUint64())); + } else { + return -1; } - if (v.IsUint64()) { - const std::string tmp = std::to_string(v.GetUint64()); - return triton::server::GetFeatureMappingIdx(feature_name, tmp.c_str(), tables); + if (n <= 0 || static_cast(n) >= sizeof(num_buf)) { + return -1; } - return -1; + return triton::server::GetFeatureMappingIdx(feature_name, num_buf, tables); } } // namespace @@ -80,17 +82,20 @@ TRITONSERVER_Error* ParseRequest(const std::string& json, TRITONSERVER_Server* s return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, msg); } - if (server != nullptr && doc.IsObject() && doc.HasMember("imps") && doc["imps"].IsArray()) { - return GenerateInputVectors(doc, server, out_doc); + if (server != nullptr && doc.IsObject() && doc.HasMember("imps")) { + const rapidjson::Value& imps_member = doc["imps"]; + if (imps_member.IsArray()) { + return GenerateInputVectors(doc, server, out_doc); + } } *out_doc = std::move(doc); return nullptr; } -TRITONSERVER_Error* GetReadyModelNames(TRITONSERVER_Server* server, std::vector* out) { +TRITONSERVER_Error* GetReadyModelNames(TRITONSERVER_Server* server, std::unordered_set* out) { if (out == nullptr) { - return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, "output vector pointer is null"); + return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, "output set pointer is null"); } out->clear(); @@ -120,6 +125,7 @@ TRITONSERVER_Error* GetReadyModelNames(TRITONSERVER_Server* server, std::vector< return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INTERNAL, "model index JSON root is not an array"); } + out->reserve(static_cast(index_doc.Size())); for (rapidjson::SizeType i = 0; i < index_doc.Size(); ++i) { const rapidjson::Value& o = index_doc[i]; if (!o.IsObject() || !o.HasMember("name")) { @@ -129,7 +135,7 @@ TRITONSERVER_Error* GetReadyModelNames(TRITONSERVER_Server* server, std::vector< if (!n.IsString()) { continue; } - out->emplace_back(n.GetString()); + out->emplace(n.GetString()); } return nullptr; @@ -238,7 +244,7 @@ TRITONSERVER_Error* GenerateInputVectors(const rapidjson::Document& doc, TRITONS return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_UNAVAILABLE, "campaign feature mappings are not loaded"); } - std::vector ready_model_names; + std::unordered_set ready_model_names; TRITONSERVER_Error* err = GetReadyModelNames(server, &ready_model_names); if (err != nullptr) { return err; @@ -279,7 +285,7 @@ TRITONSERVER_Error* GenerateInputVectors(const rapidjson::Document& doc, TRITONS const std::vector& feature_sequence = cmap_it->second.feature_sequence; const FeatureMappingTables& tables = cmap_it->second.feature_mapping; - if (std::find(ready_model_names.begin(), ready_model_names.end(), model_name) == ready_model_names.end()) { + if (ready_model_names.find(model_name) == ready_model_names.end()) { const std::string not_ready = "model " + model_name + " not ready"; return TRITONSERVER_ErrorNew(TRITONSERVER_ERROR_INVALID_ARG, not_ready.c_str()); } @@ -298,17 +304,17 @@ TRITONSERVER_Error* GenerateInputVectors(const rapidjson::Document& doc, TRITONS const rapidjson::Value* src = nullptr; if (feature == "cookie" || feature == "rnk") { - if (camp.IsObject() && camp.HasMember(fkey)) { + if (camp.HasMember(fkey)) { src = &camp[fkey]; } - } + } else if(feature == "campid") { - if (camp.IsObject() && camp.HasMember("cid")) { + if (camp.HasMember("cid")) { src = &camp["cid"]; } } else { - if (imp.IsObject() && imp.HasMember(fkey)) { + if (imp.HasMember(fkey)) { src = &imp[fkey]; } } diff --git a/src/transform.h b/src/transform.h index a70540b3e8..f481098d55 100644 --- a/src/transform.h +++ b/src/transform.h @@ -28,6 +28,7 @@ #include #include #include +#include #include #include "triton/core/tritonserver.h" @@ -40,9 +41,7 @@ namespace triton { namespace server { // stores the parsed root in `*out_doc`. // Returns nullptr on success, or a TRITONSERVER_Error that the caller must // delete with TRITONSERVER_ErrorDelete on failure. -TRITONSERVER_Error* ParseRequest( - const std::string& json, TRITONSERVER_Server* server, - rapidjson::Document* out_doc); +TRITONSERVER_Error* ParseRequest(const std::string& json, TRITONSERVER_Server* server, rapidjson::Document* out_doc); #ifdef TRITON_ENABLE_MYSQL_ODBC @@ -52,7 +51,7 @@ TRITONSERVER_Error* GenerateInputVectors( rapidjson::Document* out_doc); TRITONSERVER_Error* GetReadyModelNames( - TRITONSERVER_Server* server, std::vector* out); + TRITONSERVER_Server* server, std::unordered_set* out); using NamedDoubleBuffers = std::unordered_map>; using ModelNameToFeatureCount = std::unordered_map;

gV_IY)iLH;^an z?h?+viH>xv2A#L7UjX3vUpJKy78f&{$05%TT9>3>=gTJG!eIUih&2VyismYVXQDP=A455RSZ zADJR3&oYd=90u4LZ7Bv#^zk6aL01k7b0lC;lXa5p=_QJ%6l`aza6$twH0&gOxlTv# zv{W8@8R@6Cwe*o7O8O*3rRgQuzQpXOo7$s!tI@pZgJ}Rg5JLDiXye2UX|dh(R?B4Bwz7 z2bCu58wEmGs%3Qa4~Nmg95j1->4P;WB|&Vz}JHD=}Oc zEd`_FUx}zl(*+nV*MBaC%cFcy4-d@YGHzOExJ<*fWd>sscj;NmLjktW*X-@$GWuKx z9$_vr<}Y8u$VZxm-S>@n-zegJ!-)H13&QjJHR672#QjeZ_v<3=pYw*-=h=w+XCl%+ z%6mxm&+=E0$T#pPmf;Y2COVWkE%J}poofD(0nRD;M|!%N=TDE#b0L82@p#2^A%+Sh z0Lc0IN4mQ%Qs&$kn}6hb#j_E_%Rh3H;^F=TzEV67;DYJ!U47>P@w^+Ephdi`B*D|33qcgDxu zE9s1HOYuosQU3IVYn53=zW7X(nU6=vFAi_%Ar9YNs}#_~7s|jE3LRO0d=`=*X(q~n z^E!74Jdbr~ybx^j71+joDE3^CV_~9u^O%$^4o!+JnZ0X z!J`Q_$_HZoKPR~U#}jM+FlZfl6fOfZ;l)xF%C&b<{akNKicrrpFm8eBXz|EG_FjjFzGxcr1qMuvz6b1$ES$r!0q5{tG7&G*io`2mpZ+@olkCu! zh(vpwALG-Z=BEgT_a2@zYlr4XGVv9v?`!rVzDq008tQ)x35@ZF%+JH8fp3g^R<&RN zhMxZx{0Wg1?lJu;o>=)Ke|ln!vnRxOXn8?i1fNL1G1+;!UHdIUjJ1me_I{r58<~?>yLp|r}<~rasH5{>$LuPb)5gW z6UZM7Zwvv%VDa7p7B5!k|54ASSoj;P537U#98>PV&{!wNL~FWz-60YcR>c1`&3{rI z=MV1{bz1*Xb)5h3s^s67(?^Z^w{RwqoJIU(;REz@aQ{}|+c0d2XvebpXKCMJj-r%f z0%?1!-|65uAK74@>db&a+1l11q$2X<03NurLh3r=K8wSoBFZUK-bl<;+8RrvM3Lc) z+dM26qkPG~*$mJgWZ^=-V-q)reUS~JEn+@~?2xqI-8$A;U)GBoY1rbzc*&&MT;@K2Du~I1WCGr`-t`uMdlMDRbgFcT+siqx{a7E1oCutIX*h%V+;E zo@w%#Nar)27074xn2zuHEoBwey#SKaO4l1IB9LN+avzF=Utag2dd@TOH`dvT(lm>7 zw(ESk^KvA|{s?a`!tMF;wqgF^ZV=}34_~g#nxDAD*-*$c>1(-XU&th?ycYGxJ^W~k zcji6jM44862ifBaq6!2)S|1t830!ce<@h_8;kA82X*blrA9s=T#;0EkgHmh}!80y9 zIsHqh<5zS4^bCE>r!+ZqK_wC3B67O=AYvDlPM@ye0_4c2h9zIgH8;*j7D|AFfvOKC zqk?6wWzDele~0RWHI%m>3i!s^QPpUxNb{%x=sy^#X_yw!ma9}PLmVJAv>vpa5v*#= z|KdCAH?W`6N&+iPc>_|JGeeYu`eVZrd%rF9hu#JmSTbYk;bW<_%?SK+WT7&rM-p|P z@?~HP_RIrr3y-O>$JO-X%2SYJS7zZSJFzHBo3D5%{o1wF!u8xCyqnnEflZaiiyY~T zl_wV1R=C@hAoSl__cjzCDP;6D zh&AyY?)XZ+*gyWY!0O<71Z6sWnfP!)Y+xni((?A7F(y(i^7r{7AIjhPsI*1ep6~KY zX*(po3H!XL-$zL6<$X`U_!dkcor$}5M<@*-zLKwmlV;Y&-e$K$gM}Z=wkBpn#-`Pl z>l$TFPRvZWXHLPUVunK-kqBe0%fK$#*0o9=_##OlCfi3Lcuk;XA(YIjwgc9_t3M#? zlQ~o`HYc?nP(^N~sqg)7Vuwd&9e4%X4R;65iP01AP~QwTfpqTcm4tE>Px&^@j*0@n$C(l%f;!K@k6igzetHkW`poMQ+(44%+@xvGLqHuo&G zTo_`)PTa2H;5k(Ac5MJ0i5JnHDYJFjD!Y48l6nwka#yajV_Q=w;&~x-0K8y*Fl0Lm zFW5qp%BND7DiB6Yh8T4DA|c95RkiO?QmTJ~=v#rm#(Htju?wZXS}YV{7~5lzsTLk2 zNZ@$`t@ci7jW-?5H_%UjICsl8ARYXCgKW*9@=YEr?7^?cE2Dg&(!=~{ls6US5n5HV zyd1MUTrk(BB3Hz+_T`x6;ev0D{F^l~x+<)jHuDW;he>GgQ2#32N&OG@lDfp4;ClE> zu^y%-`+np*n1f^<`vfE9E<6a^KR|h|qgHyBL$&s-E5a+ARD(Xwq}sWxxQYLuTL{fU z>-%QNKSaNBsPygY(Wo?+Z$PCyL8Z5GXDOkOd-D!o`|CJde$VPJgH&Kfb?~7y(A} z_|)!R7_0C6kX72Gf5D-pZA9&0f}=M9;yKc9Pf2w4?&Hh8B<77+S1a+MI7z(^<(zHP zcLNqM?_oP#?%zZtXsdsT1cKqE{7KNo2GSMrQmLx|bX!ZgF>6@XEyeoVBz`D6Ie+*P z?l@wlHyANTH6cP3TzUIzg5D5QIMO9}*u_@=I*GzyJrQtKi@sr#H7^!-Erp z3da+G?eYC=F*6e$!(Tj%otfZ~S8+2FCgV!~jqvYd#Pu$I-fzP_5~6UJQ2V+x!?T$Q z@AAzu`NobnFt_hAMZD?AH}8oz1?(2p_tDehr^~Z9@XL?zS1*+BDFfld?s$Y`D{wb6 z;ZEEra|%8YH#f-}ZtToV=z|+?!DqPE5;DYfk+{A>CR!*GwUjrD#Lbx^)e@2F*k4g@ zvMhH$o~Yi>Wr45ah3YHV%x54c1TA?@1UU2xY5louK*1Z9B;uM$)`%oiL4bmM%X@Ln z_aDjkARR2mjqVyQy6ai_2850To(JU{bQY$CFyOh9Z!n||p{Sw$n{XE#zu4mnag#BA zIVUHabLc{#_t637bnKnp(eBHJp*(Uvi!%he78Pxb%*3V~Ukc6=FKy!GzL)v(9*)X{ z(MvBvevk~vTkFDks|AQTGvONimAutjUV*nRz?J^=dqLjv@qQ-mExh$f9>4C#H)G`+ zleaFyvzZA$;=SHqyoungZsNn*(12=9ZubqsHCBYA5dZcN_VB(94< zVJ+bsnMm?hk-U+-^}a|YdFv%y>z}>LwiN8gcR}7#@gf>;J@-Z27+|E@z&7qqj z_KAh3D)`Z=ld;})q$KM%L-=sOej3a8$u_$$3-Z%V zT;|C|t=*fIDtP-GHWjk!TV7*fCDDLduY51}yz8oeOh<0yJ`PGJhfm-~4 z2Bs@;!*4vB_>H#FKNdxEFyUiJLw3Y;6`zX{CBJ_@^!-k~P=4>c)2{s}f}|KZf1^(@ za~4i~wD0{FAZNLr^-jgugmHm)sH_@j4^g1L`7Pc19DYGG%cgmQtuPNN1b8ErbzzXEz0R7`f%VEN9q&EbV; z8j)q7og8-KP?GNBt-EtdjPrgL6!38PWODfA z5duQPCr^|z49P(^CNZalIMKE%Vk>DHs9J6HC40JGi z-DqA<(LBiGxZv2>^(@PjfWQ*@Mi#gNZ;bp7>czMbEm1t2gd8|i=BKJMxX(B>6XP}y zT_DFE#;wf%VSZ?iYGc}$`HwU6=ZGGG8-ane-a$Ao|5f;fgpXW%n8n7CPN6ZVX7AJFTkL{I0yN2@_@23M~ris-+>#BG^}Jd z`Oid-!gx~k-jC*YNAqtMmBrE)Hv&^IRMYTH81yiIjAj#dkmM|D8L>tg2I@R1Mzd^~ zzWo_dK1MTm9CIAT0>^=zS-7Oejalv|NXv4;Q=xW+mpdCt42qTNn#|iok^0th#{|ob zKoN625)D88bn~(MX$}L<$C(&Dx|6q+&&Rn)CO+VNoN0VhjrsV@(Dys_82PpRZB^&vPN8r2>21w&t2-b6Hakez zQhg_Gg7aKxJ{BGIrF>VF`S?X^o=+HghR??$%SlF-Rhf?+p*#=j-HbdV=3|lR#YU!; z%*TfPLn_P}V2`mBRv*q=?VtW{%=$U}KLD)-4JJyQ4%OTsj&TN5@55XYsNTm{@CT;L z^N+;CCVVvvq?c#=ZYYIihaKjV?Y(L4y|M1&u@iZ^Br(f9AG(VY*N1ZRIv?N0*oh+U z`Qvht{0Y0ZKLF1Muzui)p~bXi=NW&8f>=aOy-PJ)E6Au1w)09eYI%cz1H?Qybp_p$!0pVipwu;x7dU zrVVB&&abk4W3d~$*V!ryE+O2zVxdZy@IE{Uz`AbXI7j*(C-iP>kifM`)nHTG{tv0H@v7pxRM#>DH z0m3wo<3X!ni5%D4*M2+6>1eY9yQcNk*yYANJW?4Ohkp-m#!L0V1w{~qaC;lHhhyJ- z4>RgW<+iWzZxK(+j8rB-i*jUzn*NKc1cX{33K+i*cDUP)j#Q4X=Dv`geU#!(ucs6=!G-Gk>o_>Gup zUpUg2DV`tA7kgPx`9geAY`*w4;>DZh3xC9mf0!@AvvNKl3Z^%cP{GbS#lzL%4{s0; z1qTFc;_MG(Nl;`l_tOd%PRk3S%%- z3v6I^;u>twr+PdzBonBkjcW<0)_*{h)@pVXj7Uv28 z{4K%WSz1wA5zYcQI|*Yqy#0&u_W0xCtx!ra!MSFFL?gilBoIb+*yj=DDRUaQHy`WV z)>%KTi1tk=H8${YXuL>yKxn?@-YOJJh`1WInTlwuRN5@Yu|Dp!V%wg}3+$9b?8ezY z$#3m!69hXqu0O?cqiQ1MHFnTn?yCZ0=p;JUE97b-J9#&bh{(xK>* zaCB2Rb8TcdBbVwPRSlqz4$%TP?k5lG z?D-2OYe_WP7L4vARI}&1!p~52wX6QO2825H=CtsXaD3?T#(I!-EiikC3^JnMI@?+6 zAdURens0ZX(ucn}T0K2$45L+N{UE)2wG%G>x_YVB+ND<)1>?a?aCT(3*1FfOnNYKR z;ngc@w!b&!A~3R3sQAmP=8>7zt+ugd=!sJv3r08b(Ucbr`65d)XLsMpLi;Hgja`PM z%f0A_+BGKt%D=Z)##oTV7=@@CiAJG6*CE%?pZHaIr8PSOQ_i6|oPoX!+#oAIn(Wps zeoakjP3wt)nYsm&l=7@jrWb*k{Yj|#>b@zv)ofoIsM8&nHQO;8JjW*E2E#)lgFh*H zk6)EbJ6VC5y0ntEvP@e-Kx!V^Eq+>!tS4r^XcNLTwa}(WA!Hiw>^A;q66n`0YJC%2 zn%A1`Uj*vcx#@wKH}b?pC0^sV!~Mt9G&VCqhu2W+3J5L~-D1wLpXWLHhQbNuDFOnl z=W7foZ~Kj*a#1Bjan%mzH$1X7M(dV$x)G4z z6r^8Dy4l5sW5g+oz6VziN4Lw*e&H{Ttx3P(@%$k`GAqBd?$bzCT_dh#9C{5c(K4J7 z4pgkIEdEkkRFt#EwV6uyHjS0%Pe}2R5^#H}@3M9dtEBfMuuEyKZoRHbGy?WnG~c|` zoHJYzB%RH0Vs8-ASw&~yDLvP~QyO3=Ja^UewLE*QqxAF?>)4-X)sHRB>xQD7C$+1G z(EA5w=SEl7u6=Ll$yoT*Y%d7hv8dn5hR$Y@lyhf|##QM0c^WI(H*l$ii8xnK1?=8j zCLZZ0HPFj(&FUxw=yio#}7=FyTOLyGxCBL+T^|4TzSOSUwyy7r(H=DexwdA1NAis)J)KT^5|OM(lU!S z&#FGitMwMQ)@34z;;*fnLGvHM54!(KS-fm*?Y6T6H#UiU2yA9Qi?)XgSW$nN9J5QC)@qC_{Q0#|bFY1Rl{R4yxxbm+WF|_^_&~uTNCT~W92MwgtOgpA0o$@ z5niBj3)+>t+b#E;S5A&Zi96esyVxyvyH~E4%5i5&n|4oe%U$M`+W>ASPH9)Jr(3Sv zE4M`D`nD_Q%u30lW*om&xvl(3AMaeZ+!xHEDHl<>e z$RFSj0KF+Zmnxkp(Yk5zE>9-5Bv#7_pz9H?joO)gP!F(y- z02wL3%#BH2yy*N_prVdwv%l9G&9D2l){U)B57La*Tn3BfAYMgb>#WN7&ohU|FMBYf z`7ldALLfeR`qiH1qjw}K6@PH`mNn)*hHCH=yRXMZ76fEnWl{SD9T67Qs71f*gQE_{$NJg)84}B8RU4=a)jIQi8O4nEnalvOBMuY`qFwE8DOhZL7J&3F~nC(Yupb1P;%9s znWCSB-0i$}-gHZ3dnNj-#8h92g>H!tB>Jn4&MF~?TCa{@yCoKSCDw@!j`fxJg06Z%+eQ4{lff%J%E`q#1fM>)|fwtpZA>r_N>C zf6J#-FSEXqZ3KG zdx-TaMR%_vTg*zQx9M)Jv&k~ZV(3ll;jZXy9ABpB?P|W?6}>%B+X204OYt4_b|7+J zhxGRRO?EoIMU3;{_FBTGC46ZQvfKzPZ;;lW-fnSgnL}=h-X8Gb`On=FcX%ZXy~)kh z!}HVK5|h0WhThKhl{myLak^K+(3{pEGZb!teYW?}8!uRT`v&>S(wjbT7r0I=a2J8;2Sp0>b7*A6 zlqq^(jguUT$M?67OZuFXw#e0xm87?G8f>S2YwK@MZ<9Kpx8qqHQ}lK&pUzUVXH^gM zptPr3ZH28iFtb7n+#gc={=%e`Lwleq*?3M&GggW_UNu@?~s}fXzwu5 zUSLl7g&7x_)hBXvQn?$&I$1K3<7FD#3l5$r7vF5?heFOdVUm(!}Y$5`S?^ ztnx~%QVE?!d#%LX5>I+1w6RNc_mw!uEiub0Aw_VanLl0~gWVFBdnGPci50#Qd$}dT zUWu?uJj5R>&vvlh_4A7MQ_*Yr;~aQ>Tl&*pRi`8IxstX6|XFxa+)ofJ&-}N>DI>B)i4V^_L^1(k;uTSML$s6qvna zNY^sFbNv3pZOcbD@kQ<0>FE52W?sKr?Z?xH9PfQ&>faWay^8$Z7RjZjBYQ^lgd;*I zPvs)h$8l~XK<1$O*6M>o*z#!a`%^|1U9AoOBd-8~+9lY6tsTqOewMR`mPv|P!XlhI zDh6D4h-y=g+0modX*-F{Ji6E}>LTWh}dXK%VuFSO0?19n9NRBqd&khW!>iR^J&r z(FE&FoXD*Db6E$>_cJ#98RWtD<=MUX^(hYO^0h}mPp0sAAAcVFYbZ9z30>pr{1stP z`6y|6S##nXVvMS)#m)uObZqAOYyYtN6H`-164gDl?eCwlDI=pKqZHuZ-uPXuZu~_@ z^g{n@r{`owI_o?l*0pBPQB>o@A77d~U!&z!k7LUo%DOhU)qDVs+3`rpGQSmGO?d13 z8Er6jVh3~P^q@1~uu4I>Y9DJbG^6h=K@J#w%@er%Q(su<6!Y!sT>c-A#ZffG61VRvVBUmCpDBD?Q1s_h(-A30==l6$UvAtnemfa z%gk&>ie*8Cwr{<$*o_@0-gMhbPzHlTaphU3kA_k>+AsV40?745LbA|`I#j923eOsfCRMq(Qix`8r`*!CzWKL>1HVU5rkuS zV2d*mK&iJ-IX27lhQk*EAiNJHcxiu5(p(1m;I}@+DeG)sT4(4_m-wL0C$%zfrYi9?l za?IuGB89AVB$ABk#l)J{-2w=rnNOoz`Z=&pOhX)lAHijat%h4;g`C+Qovhl;fhD-B zFx0uHtXgX1t0k9u)QWjmpO&W(8I2wS<&iW)WQY_(NiYIiwZsreuYgoH80yx98tI^h5c8r1o z>}pZx;mPH;)4A%jue-n2cyDzs;$v^hYl>5+3cumG1Np(Fw_x;3Lu%rr#GSa#4-F6+ z75?=S2Si!*0!@5ErpIAiCg@xz$`=Q8xk$;f@8BYNO%~Ia6F;~EOuzL?i|Iaw%&M?t zPzuX|t4l^Y!El*5e!x)C5*lt^=!^rOy@^;j2;9?4ioZl(XQ=H@)@@xIm3)lCt*f3Ca^A9s__6*V@xq$c+e55AQ=S##%X zWIh(`a5B2_>!lVFCv3BLU79>sr#%=}mr3dj^piZ4vy5!x6-tLwBysy)vm$uQ-7Dmq z@Oepoi%SK3S12fL1dIvEnrR8sAHrEjbQ`z=a3pIeiKQ^o5xV#m2)A7i)UI8 z`2f1L6bil_VkUdN^U9ZYAh6JuK%MR}R`=~x+%^&1(b=iti%Go6OFROKCHt?=)vqBX z5uvCvQ3x3?d4#a6PQN4*T$~tDWKk=`{_+O*nJsGK44(Zv5*MGs@sfG-4ra^U z@_hKOCvXQ55jB;*6IJ&-TSiMJkVo}z4Q&{1)LPZ2ISwkIr(;e==9dO~RP`~Dnw<;4 z9?Mr2Ctf zhgJU}I!x&Z9d02Qs-W|o=rH%^2X8%k-gu2Vuc7d%U_U`e&nc#*zcY22AYc z=usxzd>U4mJ%(Wha8f@%`1bi-Aiz5m*eO4FY{JeFptIx_mqYv;{2)+g>?$lz2-H0& zbP`_()ZMK|qE2lh>W+0!w1A4jK7<=PV-pYjykoq$$)u;@#U<_W!rSkJ*?(C+aZfsa z{L9z>I|a3`FlfzVS7y$Si}_vFmbSE%ri+{Xk10P$=4zC@M%H(0RV+%iU*{ zIf~~t@dk-UEq)w|ZZ*7Q;WNS`gFqnD2YNHd^gG3)c=q0O*&qyT!(flMju^!8)){XV zN%E5x`W}k+!f;jG7u%1&83O%|sMqtr*qtl2BJ4UA=COr+EE^UuaN%U+V?5$L~;AMJI1#|X6jJf){0(0|5{euPN*{rORhIrQ-7)*q+vXTI z>8~a|4W0f&ezXGd5`mbBhPCBb&6gteLr=ie)ww?_{ z9Qpq1**xaZ!=Hl>P2o(H@A)kES@?4R&+YIhZ9VJvH~)GzOmDkd&#p$4`Zwh3TcEzQ z^=$ZKyFjNS9{-N@EMx4>(doZ#J^TF3j?w9Dlb(i7|KWP}781$#U(YUO5toXkM06{S}3qnX0&L`&avme zZasT?bjR59JCmM(2zLX}QiuJ*doulu6omaAJX{iSNV$YmmU~lXL30W-b){ z3S*&Q{CX6&>kQk&-Q2l|y$bqx8Kzjcs1D5ik*dtk+B=8VU<*b_P25E*O0|N7_hv~31BHqWX zhQDP-YKL?V)LnDX##X6;J-Z9E%>`z*Dl)8-Y`k~Dj7`iQx1SOVU>@q|)|G+zo0^|+ zo}oLQK1lr;WzFWI-xh9heyLquv}Ii4gENd@z4GO8xy6(z++xC%at_3jF{1EoXB@?K zD)cm^jZxH6=MoS^hv_79n&1qf1o9OZ3x(oCxtH~``9}9X@Sn~MbvJXdbmm-_)_m1F z#Er>ixu6gZlPyF7<$j#M*n>CG13@hv*P1hResB6`?Llw_#8539J0dSD4}aQEBV$9{ zCviq)>_UP^P0R=tZ-@*)e?>@QH*Pc2jz+$AX`WHHpEQ!is(3^7yDBUoaKjogY&icK zxY6lP=KMZDM~7Q|cnL-ic(!w%PL8~=-maWTb*kmg=4ae<8QBAdWBp^49yLOyfa4U{ zi!R}gSKJ&q!u@Wi{d#x=tra(`UwlEohznzkEKJ9KNv_+m_0Dt^ZOY!Cru5IChZjFc z!=#owRZH_TNqV#J#k%hwhwEsN&h?#O6Cy`i%LTYLKOkqa+@e93ft2w62_wZ*@AE z)!v*#>O@xu!tN2`a>k~ETN12WW`X|*aR5h%A)UE;Cx}L=D&v*=`Hpikj_QMQ`dY!O zY%z&!o0U%pTp4Z1R%M>4UREjCckI$uWsjdSews_3{N@^L`r>pYr<5{oplNi? zP|D8c{vg*6vLJCh+Xe7bh9OC#lb{SR2AR4@*RVqAMknLk{fKW2W~$5qE`}#1Zoc2{ zMAbCUan2RC#7pkKm$Sg$a6qAg&isZP=yCO>i9S60j+0&cBXgc?SwS%RrjZ`skWmDd z2tKYXpT-hEJ$FxyJ6`nO&tzrU6z$;L1 zZtkmH0#I8FH<$a;s&oBj-8WHP%3eDd59jY?LcHn}`-1$QoYwWEB@nXY%`?>jTxpvx zPU8QcGXwSalWmMRO$)SSL$?-EF>Z+A1FTVXWprC%qlpP?!o`Xy{{#6}zYj5!FfH*4 z;=+C<#I60hG;%5N1%mN0nUF{IozzsBK^@-5rY%z)({!U-(b;JgJx)c<^F00#@vaWD z{X4?BmF&b=7rvUGY>fjyCx&U(i%|UPg-k9}9IX9#hfbgeZg%GVnX}+uqk47D1+9|y z9#H<5BjBJElC`hSV&pzapp~Mb)MtIQYM1BWLupn5r>91V6HYib-U(%HD4Rj-wv19a zl|Glhj*etRjxAeeul7`@T-G_xPqTBg)YT;KOzaIuwd1UeJ(90gQ2cvHnU=vw zH`SvmGn*fsB|Hf-ON13;ti3CLky_%`n6Ud4;O^{~oY$*T>z9p@j?qpUeu7h*g1Zs& zM*IYj6Y(6XIW+X_%P-k~p}BY2e?Ri!7vI!=ni<-GgPbkC{keROrGr5pDl)2X1s+aF~k&HOQUOdKmlf5&S6 zm|Nzxwu$N6RN8P|Mm5)Nwm7?k2&w5iOS^#dIl9oDK4bV{SwOt#gfa4USnvFp@}aH& z3e~=u5!(8mJNWv}P+gl^`nH|v(lwfC6WC!r(u=dX5*#=52CcoJGpIdku06gL#&mLP zB+prl;R4+qH@7zv3!qkv>XniB8y{M_z#3v);yxKSq;SzYkN4`!xv2KqDLXQ-V~&2} zd~R%QQ}tb=`p!<)7bK9>xWp?IN~_S=nCDu(cu9Ky2CBY2+thb2g}!Hf zA7I?(AMaZusc$~Zh0E_u|K?MueSN~xFKl39Zm_^ElQ8YDxAjiza8?^P^2>eHr`6_C zBW$!doyh}4SnO~s0epHH1d|xvNc~1mvsvrsiZO{!?z^+6=;R1>C^h5m`j^%|_S-v* z6EX6a+Q2`VrfoN9zj+km@Ro~euY)7*i9IP1ngCNZ*Rg$(>W=zy5_YZ1H_>yG{n*F$ z<3Vci$&>!^``T{>#ggM6;f|j#W1}((w;)rv?Z?WW&R|Ju)BgLI*0gW`&R?5(=)d#! zee*xv*M48O{bnO9V?Ql2Y&-w|!mh64`R{AneN+oWhx0EZf5FnMRv6RAPIPLp2GON3 z=KeN$Z@_PAN>%Vb>G-)BOD96V;qA9E>e8Ci(gZax8lu#n=et``B*>q zQamx@&uvx^GJ-(hk70yDc@!PqmFwZME^gFEi&HaX_79`WbaZdR@{~niE*n`HUBaF= z`t<_YNeMzu#VM|9T6uH=!u-;3e1byQ5glUcns9W|T8^s~bT)!}_+WiZyvc310I{K1+B z)}reTB$Pul zL1t|^ljXuHclkWY4_WkZBIN$o_VlTR!T0@crT^LYQP}PeZvRjV5*9(m{&S|wUqd;L zd>&tS$C1ttS&uQ6Hk@$f&l-H~#SzoyPvwn*b zo9DUh8Gea=C*>tjyCImi&JCjHxHIIj^UGtSg4wZbpaHV+g*zE93N$z%0XkH)f@ful zQJh>*sTajl3l}#MxsHcmZ1^DQi9-XIJ6ncn#Wxj1^1&{==~zaEg&}G)XGH|}J0_U9 zrHp3rUL(gTV@S=%dGnNt_BPPb)qsH;Tj+!55PopbZs#7=st>ylvB+LPOs&Js^&}Mp zKARtkW#xzC)kM_AgUjAGXOx4*tEcQ+-tc3TZ&~@3SWo6GD<6=V(fq8PzcS-KTDU@< zDP|duX($wJe$nnL373K-3VNDb^Nl9geC|H&r>ot8Y? z1kBxrf`@r79-8WHdw6ru3Vz|v=vy_1DK{XC3Rb>*afn3)h|EKUVa)HCBr3oO;0G>s zj9~xj`f))Go1JfGA>t`MjPd_4O6=UkiC~LUD-?^DENTE9j)(0UMjKt6IBYi$6BZ|q z^F0se*^9pkPtu;@43h9K#MNzdY4BMuB(%GIAcFF%+>046R3SDG#j7XpliWeTVtl&@ zx-k(Cdyr{IaT>Px_`llQkYn1bqP;P;l`d-G4JF$ax|v|isy%blVyfNSR{Idu`p(yc ze@I}Af*Zf-{S~Dl95gNl$-~iRk6*;5RdKh$cynh!Fvf#GdBR`;V;XQBR_f)Hnw)%_ z1IpN2Dzc3&*I{Fs;H|-2G*& zqdOlCF|`k2@X$+BQW!d-8e4|)-(-wz}; zF_e?(K#BBeq&GjE#-G^3mFj-YU@|N@YCePkZAd}GMfHukd*dJF@gSNOTdV=Wc=6~4s!^I0DZ-(Mkvn1QicX7SD3B~@28d)7g zxnTYUg8k5nUqvn3i5tF7Y%97ItAhhRBf^&AhX)t7bb=D3AY}uwoxL>i!(VXmjyT~I zx_rWrhx=oPudIHN+VQHV5n_i2YuB6D;a&?SZ{YS0Vu!CZ zcS{bYgN3gtdid>&GFK{AHj(55bqCwzYbJT=x2~nTCQCOs^2M`CNaj|NXmURxRk=z? z=4vP=_a!%13CY|7pkZ?V=H@CPnR~yNJIl>gLNYhEv-vi1wc6B6GN;DPIZHWuN#<;F zb52r@UXnTEvuthLUs?OCUXnR0-JCs@qZgZl)Ivy+!bWrd5ro#|^ZEo?g;&FC;b>%b5u$>>CC7SsR=?j;DvX0GIc{h5wP4AE`UxH#5mVqDWg(aq)YG2BkD zB2>&By#h~JY%iDW2%XU+kUDQ6n5a*$5>>0Nu{x`)c4=k^;SUC)CadG(m*r^@s8jl- zQ2ca4Brf7UgSR4kgrcuD-{H~gvdF!bUO6v7VfBf6wJO-}lQ3HnOPkj)8I{o&M3bR~ zpAgf(*BkUFBRmii6-{nt{rR`)(Nt_=`M>YR^3O80iyp%RSGx4L){W)Aj9C7!RZ7%Q zDD>JB2-$xa55f4!lL+U5aw<^w3#O@d5E(})qqb2Q5|-=E^)kAqGRzl2FQZ$V424rN zRVh}vTb21@Z!e>5Ro^0unJ;w2<8S;!FXO{D883Pn@3zTMxF%DTlgcoSD{jBZc)m?W zjhk^hRy>WFix2?oCz6zy#~&~_@eaS5Da}T`u&MbSb>`yVv+F()&UrY3+^>FkB z-FFc$xinN+K78otg3CVBu4oxOB zJAG0{H|^if2lv>Mvyi#-bfVbzL?ND+fP4 zpKADBrRw}S#X`{+o9|8K!!#Ob$@$}UZhwm1(a1AA}WnCmuN1{{Vm^b9$`lv&EuWapPa|j+s)^2kQ2Nsj_GxC#?*6OAB(6&cY=p{Uf-5M)eOl>8oJ;8le3Za*_$1 z7u}|H;4R{aS)S~`le;ZtLg2{@gA3QeleuHPMhQQhFE6)p5>c{@oD`~E>+)sWg5}HH z$ztwUS?BU)7KiIs>L&TRAz((~irT@Q0=F;Bhf*!FD7~EH?5_2CQG!* z7ju=6%#~PVa+^t2t`d^D5{pdk%Wkd`lDQI#Os@PM^sN$-xe|*^uEL)xR|&~nt-a(% zrYJ`*$sCDB${D8|y(Dua8YyR(a`ckSk!Ylxfy&WKGDo73asst;^+>w8m}Mn`(tU{8 z(zhWk{T!5=TZWBf_xc8EX8+z#<4ncE@$By&5&pJZ-3 z{7L4t!=GeMJN!xJw8NidP8<9o{rlh#vi0HUAyNmBY5PM~y)oohk>4dv+w1BV%0_d- z)m(+et+&t`Q_)~B+I7J;h{<{AFzHH)X02geRb0lYY@VJ{_WC@?dvPba}tzXWwbBXOzXVpO{ z%?5k7GRoBs4v%ZLq)B5gzKlX;1gx^38Lle2Vsp!dS}KaenSbZr^&d2NxDaj0%ysh`uy!&pf?n+ui_V1PRoXzoWte$9c4pYtpCa2Af)$up< z~7oLL`u-R4gJ?-c*u=@2)(IAbm`PSIYc_E@4OAY$rRc7jnxm? ze8>&jqyFx}7!RsEZgZGuNYu?4WO63k9N^gYHuN)dcq-^4Gb>=DUb+I7vxFsNyAw2q)PWiwMW^;vqu5d2Tcy?qv&E zM98);77@0*mO_M&d3G{wyJ?6JCu3)bur7a>h%nz2**PNIYx6rmggTqk0U}&tb2>nT zDsqxnmk(1h6cI44`QpJc$vTQO|LMNVA2@fb-!k57aHD z$~2w$=7W;@=I7NOt*K zZ{x52ahC=Wr0S)>)IUvuHPuXNp^c5FyN@dM>6@8^I`a+NSgW6IMubh2v5)bcZn^Mh zF9hI619{NG%4}KGIdXWAE6Ji+ zH(~%=ug+p*aCHwp?i3B5kcIoi)mhQ<6LbPzu0XJj%`4sh>b#*$5nzqj!&a{7Pyc{S zW4#~ZC+%cz0B5_=_11SI6SEGSHtxAvXIIAaBv5y+8EDbz_=?D!=@#TBo$H$W^x{Pe zY_g3J>0D(pBPX!he|WLIoah{z#5june3-2wY?J!H6fo6Dki8PiG0bpui*-vVERbA5amYJxw_SfaV|iJj0R^}O{gKc-Sszw;xi zY@YAw;p0aXG7M4Im7uQB^I(_|pFO~5&dc|65qsh@7;8f9&{pm}5)BvweDDb3nd0%@FEW^n`8@%(25ewM+)8yxx`J&w!nv+fG zVC>YqV63M}?Cg!#X{NUecBj14gTBzO-RalZL_SPeeAUE;JCtKt?X?Ksbn4MOjvQ6^ zsm^1^qyZW;023loZaM`X|1~#WYezCdO8^&qY6eaN4j4X|`~4~hZWGRN`{uSlVCJKA zgeRPMVmxt$S68m4k!_^PVC%WjTr$ewFy+Vtsz*qbtV8`|tUbHnRO~OgY*;2&taTb% zYU6bc{b``?X=2v|bp#C3_bm zHPe^G6(qaxWiO?x1^N`erXJ=vzXdj7kO%qF>?xZJCh3JyAUtp%*?zV}ALZnk=zZW3 zYH<O?9^iQ?TbA9wr*9`rGlLq^lb%R|PhtYTOb_=hAd9(2P z><s#lRaSqq1K5N**B)w&B&gg4aOm^7HG9sn!p#8BG>m3-?Wf z*XF;|g#&<-;8hKTQt(<2Z}Y=z^cELhCs39176fMDwctl7c&+$2rH{vKgmFzdh#3$k zg7k}^Y($$IkAim()J`T=auw}sLz9StM``-95#}q8-=9D=&XLj6K^(`@z_)TA}gxa&57ph?`HLXN7MgvV=TKjfuE-K1lmP;*K84ZS>>4 z^#l&;uIQHEo+Vah`R&HSMw4YutzuOz`#cQ#qWQ6N=SnM4{N@+J(uHZQ%|bT5sXB?e6mU z*_>~^hMja|T6v64BXQkuKd=z{q$Pb)X^ZWd+JYaOBcQ5Y7X3UK z&;m!sO2H_~xe(Wj2sWYO+l>^*4Sa&jr-g4DIcMlU*-K?Q_e|%E&n=m`G!&~dW!>2f z7B9Z)4XM`zQPCw9TT@Fz)*qnKnwbZq*kzje?JU?IKuPVPYA>OWpUw)!?i3vO#KJ*# z7oBkSzWMnN1c!$OhbMUq#rEG=Gi_2vpibJ1aBKuN6Q^=c%8C3_N{FuHptE^T=A#j# z1dJJ8B-M9Tibd*NH588BV4#;YMUv~81tq6nn=xa%`5Jo2Ss_j^=jF?s7~Mm?38J`o`orX!i=NaGwLM^e@o7pV=QrmxpkZaf_E1ZwlOe zCAmagXX`u6(|MTm(EFdZ{={fK0u3cRSH^}eD)Vee!1@IiK*ok7Wx{1BT2!M`Lrk~= zHy;Hr3q?P(7A1`wvZBA}B8!q9cIpB*n)70=s0h_AWctb^14x3c-QGE0l>Y;QG~-#p?of3T{*;(}}tx5HqoQ;cSv9H+QC?i6p$(- zA^>JG-tjV2+@G=7%Q&n}hJxgqst#?Fp-q&@$WP0NTx$~hrzig0Bp#BU_|p#Rjg*+g z0sfNPBLht0!1TnPO0gw zFrN2{AGV;WckWE}2Da-AY`r!N+y^cWf3$WEkiwBTpIeGi>AAUFaTQN`qGF(e{Xi z?!NxSHzO9h`=lpY==Mraw9xJ0Pu$K%JPBPLA=lNjJhNswxS@EW&CiH|ABvrpr^_D9 zUUZ5wnY9e(93%-Vo75wY7={jG*XnTL1$Yw=qB0SSM;1##6t!2kQmA8$?w}&w`DRjI z_ovD@-K4Jer|J$zllqoF^+GT89e?U+Uh3Qa)M797pZ?T?z0|aNyL+i?{AIWDV*2o= zFSTCXOxX69!5y{+>KpkwAuW%P%A_XG$TV!Tqo z53!f}_mnc+l#$^gDeo&~s6XW+r5xr@ag?GP1HGBAmuo?yi$7(fQoi6%;g-Z6Ko3)b zwG7Pcgz@_?IfShji{qRkxt$e=rcV3;R|I3FiS)DxZ`i;MkC?>9>d6Q>tv`vEADW}+n@vB7 z9($+>!zspq7whzWiC$5SU+1NHfgUoH_Cze~BTz}8O)_^GoZd}1J2?-0m4riji%EZ$ z(V4X7=cTWkoJU^s`@y;1nngG#?$ufpjQ?1o45Bfv5y$2`@AH`)4{-bmW-d*Pqj>On zHiG&$z4Z{-+#0S-s7XmcKcTxT3LbY{W)O+k4|oQ2Quyy_e|4I3#`$vr%Ini+)| zERP%?EZ%=v^=HA~po|7GczzadCP+U4Mt))XQO%rhM5(bR7}cW;;+gtVvm=od%9%(C zew@#rg0~Mo#3|PiZ3CL@odCrlHvJa0Sx74A!rxJ}M!5ch={V z8um|l>YQX;b-a>@O2pyVlY6Wp7YAmK%c%LfZ{W_xnyB?bFAB3@J zo)d?><>}wb&X~I!y(ffzr`~AGq&EAqm(*q{xAD!qk8tkm&Cb)_D-PniF5m+{3C1t& z>`dMh4{KcyXl`Ix?@XqwZuCNtxUD;wVN_|=%gwtG&v+|Qi}Mo-5?X8HI8MIivQ8>! z1vYz$qe-{+#>SO<06EU}yYXeb_bulV=8v1!f=$D6NCpLSmzCyZ;wY16UpteNt4EG8 za>uqk|FZtEM=6%oBu;W!czS<19&C-3f<-HA<(L?c+9QSmmcuKDPCy!zUijAUo)jlGr?%7<@mOf4i4%rEP{EUw9{m$sa4OZX$ zD@z|D9YZgke#oT>W0~st>%_V;Bzyj0uq(!Wprtwpa_@JHbp<>r|87fWY@bJsfDN+U z`kl6MuC=8xJAIw^l-X(XKECr4GvnBuKCU7i*quhMlAWZVU;Jlwrwz38UG|bPHTCQz zuUKjACEcM|e&gCpR#DDxlIqT9DE{5{l5SPjUedMyKfo+PM`wHVvFrKN7IPQ#X-+FT zn9o4nU--D49q(zry=ikWcGGeLj$PXEMy4g_vPg?#S?xkupI1L28(#d*d^WsNTKgx= z;$zx;sfg@%|K!6D*M9dy?omzI@BS&#OJFkY6X)%3rtkC4T8BLUiPB)~hDHtH@Ee4S z+M>!w+{53|rSf%+*}T|IrX&tKdux6C9{&GkS^0ku->1L)e*@p2*w+B;q)y)p-;GiF z|9|-Y#fv+`_p8{hAhUMP{NKU%oPmaEP&_j~pZ2Tnz*U>vr{X zf2HF;AP$9*w|_|e_rRe4H|vjaDD?yDXN$nZJT68Pj?HT1#}~umNH2zkT~EC@7Pkk} z<1l#h4P)*gp2ZKZ|BDg-yY=V);Qr4tj5yYy`SHcPIMR!G;mwbYdy!@8Z%oF$NX<`2 z@h^V({M`Q2A38rZ15h>Cco7EPH8PJj@9s9kbBCL#(`gAp?t10QLkL3EdndkO^nN*_ z?siQ_hwJncg$bYpM0x`rBySq4@Jt(X}|e$R9hdc2lNXzg^3J&9(&IcIY=7()Qza8~5Km zziECr-*u7$*x)bbHns|hAc5VmvG;{mncFk=E_JiEI zid%l{CTZ6Zjn?RApnRfbiP#%U#8}o77$R~-x)GZ=h}vXr5mS}14-!AO_K5#;^%?$} zEwou|+u_?RYqrLd9ogT(KeD$a>$m*8G?#s*D^H$M8mrk~IK}HuYe{@q9hZX4deZKwKUV^JtV7~G%EnX!}cW~_c{*Dc; zHgyc5g7>}p6J2@N)>eG46wgNQ+Mk+O*TL9=Tm~ZF;oe3OlG+$Y|7vn*-RN4<@P|>G zOnRM+mt*$$IZTRk_g5RV$B&KB&YNAXiQS_;d{SS-_mh}4jXF1&JxlYN8gJ~*3Cf$% zB=`s8H)#KcA}tswZ}Q+1j@@DbJhPEZqlDjvRX_r2{c4-Fc7 z%bDT${`J&r>@9n;YwC1TG~Cx1TxOpXE$^FC7CrV!RH}IEE(_F;g%>ZD_2r`Ed_}jV z=11^*hM6%_2LG`5hDHQEo2Q0zMt{A5gX6=|(FBctQn5>V0+zC9V_{2mI z686W%lU~ECPZ3?>B~6`rWtdZ20?ov-OaLdD7GdV;EN%tN!hjk@nqOLt4zA>P6n7q0 zhOvx{WRq8YLdXQ>0VBxuqL0L{`r4Uss*pF`GV@VQlLse?_gcZ{d#cdf7ZI+F<0Ri^$0oMePmKXC?Y zuO%v$OdY$1fLY|&3^^;mPxb3Y<-FAy zLu-nUy}N>7GY%<2O_(VNHBW~0dund;to`%ncBWwfUU(rCKZ-Q9$!GSFnm=zQ=9e-P z%)*c9fl=N#M}KJvg>^4+(qan>>9H$)&s98kKtF(DTUqoA-RMjV&9vt=>a?fao9w3tVmKzYuW zfFadB@wWd_c1C4%nYm5FaFqA~!(U*%B8I-~C3Di|2zB$xy7x2EIT(YM2q@-U$l+Kx zc0qpbT$$p6xh>!HL>4voRJS-&kU)fRIhebAt$g{?Ihg>^gt-_01%pqg)Z9Jn=kxen zoVtXxN?n2=lLNv3B_6}!iL^>W)m^1~AAV+9+~QX82w83%*mIu{+J7jt=e#Vs-k^QC&+NMT zQl@+Ww}kjG+r#ILZu4hRU!sdy3u+j8hL`_}n?IEN=9}Bo!{6Ne{q_Am&v2!cl{+21mjuDh0;0d|TrH?CBmqKvs zN1kAGmqj$A_D=WDQj_w?f)taO?0c@`IoWkpAM@=;k5oohUCV!gBD zK#d-uz;|S%WhXU&ZVZ6AG6Pub4BwRMewMGxU3vDw*FC0OWr$CKoh3aBVYx3Qs1SiN z@lGF05dAg4&K0noeay;C9J-pp8G-tXA+_Y`?(|^hBBIxPJEdFj{FUY;S^=Dd4t<RsWZebD%sP1{g_*FGlO&&tcOhO zSuF6Wd@xd^+2$aF-k0oAdS~WrP)|!9Z(v*WA{fz#vTa|bH!3U7kgG_Xq{YyUhpKhl=|eq^9$=&$`e ze$!0XsWA(qOKLkn>q5NfdNsi7p5_$m={BpV5 zxF#+$Gc|D+CA*M5ao4N$Sjm)`Ox$gPkZdboHfw8zIsnT|%op>z5O90U>+I)JbXw$l z9_M?$)b~8a_gu@fZ^5_wZ{IP#u`9?+qiT#$os%V%$lfOo&So*|h0&B= zC-7>Q2!l`YqD@~|;y=xn3%Z|`+Sd=<+(NzLJs92MT>ZFE8=mEZKsc69G)%ePL_c=! z;9f7|lS9V_?C6=~2+wn(90)Z1JF$2!spICx51Fqbh8t^&@ z#zUPugfj7xzv0FN+Q_Y@mJ}L4%5=WPiI~>|CfWjsFwS|lz+P^F5voCZ9PXddW4xr{ zXw8bL>^M?1V=YdY-car9lTAa;0jebWFXil&s-P>60Z$(hg$A-qJ6#gJo{R}E5j0_t4Nq>n6ugPy}fP0qulSh+SiBO+-+j_UMIV< zt$oY zjqsTgjZlg&$xQ|EG8`)@FW+mC-|0rZ{_}rM9-HSlT|hB!a`$8KDS5i=45;ISO zO^Q)^)ov(FTObH9tsQzd{5$i0rAQ*5uov#;?DaEq*nJ#*)@5eh8nQc)t3Idpwt@O9 zvy3EAnsf~&`k3^7RQB3!rZU=}M*B>WsdnC(hTr17+RLr>Vahc>?#55>MK<Ab+(7+8v6Bw)=y2POF3D~@`)P`JuJ^u=obGnxUpLc@pBtN9q38j8 z=B&{o=vE!5`-$nJr=NOhEr@Y^viwAq|1;&AUjZX#QJKYvpZFTxE!k*4x6uh|bhA_< zhA#B#oUclvRPvK#CAq#zmVk^Ft-J|5NR>QFC9&+D$xoaOX0YhYyE$cqBy=%Rl4LY{ zL!2+B@rAQD;E!dm;oWxkdEaNdCqE0=&z{g{<$Olx{*e6SeD-o4Pdc<+=cehKT>H($ zZ?-rz*K4@C$^)`r0onX7gT@h4kBIwq`+vY*CkYwuX#^*vtw&>! z=8-*eo%;!|VutA4D3+)Fi-Ncsbuv6X%j{uVoE7U;3@w9qmD}b|v_xCU`yH*y+Ji`) zV`T9cjHonHpBoACZnMB64e1Ofxqm4~*_TzyJq0J*N$x<^JL6qdU7pX5#8uA(>P;Mi z+9eZ`%OQ(l4U`T{(dU?sqKiUdhtk`b^&S;4(gJNgKmVOZp$kw^E^bLlSzJ! zq+9(<*(h#EOzMj!pls=8!j&){fOf2V#1+XOQ@Dk&0Mf&BCHa51fI#&<1& ztI@Hrxaw`8g9L*PaAlOq^y5mkjs+&9RC!7IxS}*yVLeI{J(xE3zuZ^LJpsx3OpN6s3c7B}f|Sq4YetKr!8yNA|by zPnx}E*8U3}x12SXSAW8`6NlG(KZYa4d*`gD`Bl>2$BRaM1m^0jAEiSN?ry_J2t8r1 ze0QTniMUGdZ3L}hT)f*DIYPk38n!=G?TzUpWg?XEwpPuMesfPcQ%9{C8P@%%i`|*F zR)gsKii)IjWZDtR+|wAKfKc|Yx0L5UGOg~)v=8Ep-gym>coTN@-#mO*isyf8t@=37 zGM0T&ny~jrrA-;qvuEvhMB&@B=!x~iJ%5-+{~h-~iOUt8`=L8h=NL2*hV5xb7w`Rd zCXy*0!jCi6tl+3G`JkFP_pBLKex%Xu1miwf1CpmA@3Mm_{Inxy--SM3-)#_QdGvz( zZlUN)E{Lj0+MrPJSAn{jN{BAU+q!vo*Wb4(GBSx+ zJBy3##q+hE+KoEwuM<)v-BV|(C81;)k-GtW^&=I!%cPG|xggcp<`@@dx%_FLbY6fZ zHeU4U`zh>t{y90j4bMwDyWQkvI#x}Quhn&Wn`P#E8sWYe9e2(W)7NdKym)Qk z<|ydd&R>`Gl1Gfc?gP>x74lpeJ9c4N&HNgnv%J)AT+Tx%Hrp5$CB4)Y%NQ^9lUoZ{ z5dSq$w}e75j_5LLx8tQ=1S<&Q*v#+=pS9e2sbh%I{JIM_ZCM5G%Gg7VeChgj!*1Gm zFz=6CueGk5^=a?EI|#{va*lrF#18SKwtLl_87v9n^3E)_ejr2~Ysl0!rozX)gsqJ^ zmys`M(6|jR4%Bs}kx+DHVi3R9*y?3OLs-PwZ)|9FLSU*#i2eVXZV_Phe|GDN*1vYc z)HLFoP}o>ovJ%BgW~7&pAYSwhTN%?Q=SjOo*aPhqvUxZCc-IATSD^NT1&517yK!2v zBC=ae@nPJ9BixDiK8CST*C2jOs2+NH!ED5%_z~~oFcgmWpUsbqB@iIiJ7+d)qa3Ke zm5*ymPM|j#IK#y%N~vP0x+5o_*euycQPF->M8;;}nae1hdFscgQW+Mco7{8lUJ{VCud5ucPkIwyJJU71$lS+?jz4SF$s zEHQ*X$@OABgaR42IIlbUU9aFEgOHla;-fNc7^Zm9HnUGz2ym`{itmM`$?q{RO>7{+ z`2(oxVd;B+_C%Ohfa7PG@0ByyT?5pyM1LNBQYPDOgSOJ^RyZNl(gOsrBD=2UK9CKQk#i1zKJH zZ^-uGb`bW4X>j{WJ@dis+5RcG^@k7z77uPM&Wrc7gPW#Pa0^B`c-2*>(3mrs+Y#Eq zPPfdUDZib|v@Hm&y8KWGOV}|ypa~o@Gkq&=? zBntjaXKErsATbW49s=JqT3d|uPJb{Wp-}(M+piit+4lhOykt*3*YfPqcl^N= z&iv8${Dkj$k?(o6`z*7mk9ck)Z@czv!`?MoSY6l~vbVmsGtG$}k5xuLY~Q+5wsuEm zq<8I(PQ32VD=c9Te!_u@P1v0sdntN?c*zm`7-O#3BV9KMZaRR})owM{4T2x)p0&XI z>S0o3KVZL0v7aD+BX48N(^N9vH(gndxP=#HQrczLM$OhP^s>W}vZZojyI6 z2Q_qU8NN4EIyeju2-H7L+OlC2G6ltyjY^p6Q~RZj(s^zE{_`!u^gf__ zuZ(9EM#Fhgm=^CpazfvXxreBtc+oTLc|^py5V5-i6$vGxO&-!V?E?jW_NLGgd5mHh zW9?6vjq~V!;(-Uu=FaXP)z<>Z!K_IOUIZluYrg!ce^Kh$q&kni?86aiVgF$8!*4xY z5IbW);g+-|d;m*9?X=vCNS3(-oi6=}dDJ@0Uies$h4VW4>UI9YMiEOBwCEg3a;)>* zu6Ep2Z}sHhIdUXZP|0KMHg5d2=NIO!6C7bbo&t9)LR8QJRCJy_U3Mq-ob(CV3>GVzbnJ*|ijnK~$p}SljGDm(>XV%gg*_t76e)MHat0i|! z=aE||U})U+`)^O*PcnBVAf&Sm06N27O2O{Y`;+Ut&sM@(i?uro6Vc{4k=GRz!+dYx)AbGvRmov|Ku~GmMD(5C@rjn}rx}e|tBnapSBkiV@LW`wwG$n{d&?=; zRy~-up*s^ml=_z2#UPn6EP6&NH8goELB3B`o7Lx4{RGN4B{&a&6f8Y?_Ix{fi$+_V z%UER+1Q%#^t^(}df?9Q7YC%1h=hXS6N=P3i|B5!cD5waeI2GaBQq6l#UyXYws-DlA z$ctxR`>9N-G2BeUx1`eN-4&@$eReNg-?UHL?Uv$L7g+3%+SGHtS)jmJ+F0*AE}a+M zaSqke8Y`J}KAV^rklI-vPg5V$dxnrzGKlX?#fjm3>=gU@cqM)G?I>J(pLBc24z=ew z&XYLj8)N9X{RPT+(OobC*+PqSm%={{WnTu*743}#S}8{GC$c^MQTiHS=abu%R%)xd z+T|(ND~H>1IE9aFwn4c%_|S(`wyn4pv%;O?%8u z3;5FBcGJFQv7I~2mg}mWFkM}*G`Ef6ZX2(gMfuwNtY=pvMtJMagP7hqW40iWkH|Cj zjH2*uSn;p~y?U->EC12KEvlDMX}FJJ5a4p{sczM9w2jW;H(FMYay261pv5^;*^p>~ zmE%3;SZepbOKm2ifkX%|GtXr9HJRB#(-AE8V~=Ddg*>UoCFDiMSpg4wdGT4;af10~ z{ma6|=f2;{rXDHdL3J9;8Ip_{8>DGQn=CTi2%OQv(Q6q4=#Qe+4%s z<*&$%6tJ?_H4yzpvpjyVv4;i4S*#I>ZqTs1zw+{5ar1{NU%{V}dJcaliTaW3)naGT zGN@xe{#drM7#JNfUMFbxc-&=i;sW3EpM1|3^Q`^2@hsVT4tLWSrBX*sCzkyt`AuD= zwsB?aF3D-v{ZyKEoE^%dZv^U|qmTu_R;N_+1OO($c0H5PF|TSKjFf#k%cO@rS3Y9} zhZ5SzgI%YsgDO+(^MLu&GaEg4osS+zR6-95cm25MTbjKR-cNPhDY0U5(=5&zf zR4MpQFl;FW-w7L&@O7hMdobQxFgELT`}Ox|LGoO2{;5lV(_;R3?7!&lHu5!$dhCzn zO*1`50GZN9hWU11WM;@GbmREbH7sd493|+HCt^P+%8(YsEQ8or(lf))BzrE%@j~ z>i!1Z-zxWajDD95BhY1!VdJf%&akP@$+J3TG}Q`e^v@>OHu4zgQM=OqthGPe_yHb$ z+S(}+Z(4Kd&EP(FSrkXaN}TIHGq}X@Jo`PUTzknNhjzH-tTxjcjGvZUHj-F~qr%ap zT55?+O{6a3Jw*f7p^MAw!;G5E^Z`ssI65sa#D(^8{hVQvyO`;Pk92~*`QgLnLjxC(FC@6UxmcjmX{>>%b0=RC zWNEwwPJCLG+@W4pv@d2k7(#|tfGE?m%X*`?1ixXJ^JdXTQ*8}@^)EVXk`TWt7#%j% zus&QX1!3neHw!qLc>QyMvz$kOv&Q~7_QzaQkPaL|cY0&0|1kj4eVM2G0QJG09xqy& zSnqpwe9xQRX8|+&H>vV_`JQj4Oj3U+R^`e_?H2pm={Xpy-b{xjxOSVHk6Ad&(@U-F zhyoZe5TQ3`EPJUuQ=;g{Z(F{+gvDYma<|Hsx@nJV0f}Y5^)steNZSFN3D2=RfI!^< zcwlN5@Dso;u6GqcWZCJ)vNM6sv-K(92*^0E$y^u!g*gBoC{TG`82qX(?kiwZW)3}_ z=aAj8kwB+euPvuB&aR!7)5?Vk9M6Bs?Z9PuORM`fU*@hCZr+k$Y$B(LpGHorsQWPT zlh}1xu~FSBq8nn>IZiQ*AUZ533d~z0Z^lNUMy&4RbY4hzx3yN!k4?*Q_FkZoPaHse zEnq2`xq6Pr=j5;flN<)`P-B6*6{cVQWhzjABE#@4RjKt;96W~(XkX*4pE(I1e|?s< zkwSJ2eS{acUF|h&jO}>u)^B@eJc|KfhQmplAbQ-HKy-kG-V1C2)xpiII^|j~m_goy z+*%MYbk*XVVkLrHcEo?Bg~akZlKUYcO)syVJJrr9YNx`s^U4_7In=guo7>J!+Em0w zU{EpHh?2k1Y9}ayN&A%>p$Now&bP%7gC-*vt#?M~oo>$K7c}WbU*@PUL+Ok2>-nr0 zW&9Dp6Itv-s`uR2_k0-7Y4WglR%`BC2}WOsuc3;FWx0k4Y<9F6$L*u;52lOk?dH~I z(b?#i9OrBt4XVeB-dh8)ywASQTeT0tH%z;kd$lt1dz|pYGAL|28%UjdpdIo4%!*1z z%o>{gJ5?y>%Ge`j_iT9IJZQ)2<8EZ0th9i=C-sns1I=*sI=poYw?r$l%AUzKUFm+tGNUH(-)R@dUIY6GEH5aM zfeTb#hO=E`Ni&fkG}^ZP0ylQ1n{)R!brl`8!qnw_YQzT##lPYS!ZfDw`v}|Ntx(c+ zb{iZfU~%14lN`>^GfuAhvqC&OR1DE*T{cnJbiZE|8jM!TSUphpcV$Gifu*l0*08K= zMq#US0)35Da|nh12-BqBOPLh2;SJ-3?{mET{8>%s;p!Iq$fQBeVD+gs?J_zmzW}pI zAe>*Wo%E59F3$ux0llTmaEdPJzkG7d%o;GUJx^4?G*Wci?b#|Y%pZYjp z5!a7`p7ymfS_Ms#U$t`!?HT(({pSSN~av+IO3 zc>CbUlo>e!J%LmGjD}_^1wzdCvaEfWf2KKcm&THePaqSpJbb*ovd!4XmVBE{i&ruC z&ZBervyDHVATsyX6k#s%Jul_iTd&aH^hSLLVdXJKb5xiZiCWK>|LxCBepls#b{UCd z-FzdKduv@Vc055Wa?o7wKa=ObyWHGXx{O@$a<&5iYxT%v}s$aRRID=2pV~cap z|Do+o;HoV4|8Y=AO6gEZQfX2h3sMV93Q`I}?KyQQN=>_Nw3%fV8K`9{#X&utj?!+s z-S+Kfo7b!qL{M9`#j;IXof9gi<&q`;_xm&RoaZ@!cJKH8f4+({&+PM=&o-a=%*+7s zXTFX%8iG^RVTjj5wFbAL7s{}GuFxL*H$h1RwRgK9%CI4Fo5aV8foYtvO1E2&7RtP8 z0gaxHqgv`_y}G<{Fi;^?$9K?_t4=1r)+g)8GSh{9M~$5804&uBaZD@2uaUMtR&VB0 z&0@$@e`4Zo#yJNGcK9(9hLt&T1;V!dhUrn)@Zff;g)!hx>Qi%dhY9C56eQ+m#l>N( zrFllM11(eA(e+#R;!nloo_#*vxt)ZEnTir#rY5|!;OdxtTEZh?aRn2@*(cx}mbwN< zNU)N-{35`khla;6tl2rp?%+8#qaAPJIwFn-JT|He!3h@20d{QRE~gM&!N+-i0bgBw zqORy-Db%FaB5n!$N<=t<90cS=yhA7E&V&iz4P;=Q-B3Hg%C+^_TU=-yww1HFHdOSW*2r!GYA4ez`FePLafvLn59BSSyYaYd#bwpZET+rIpfNDkUj z1A$C7iS@sPS4+69s%7|zXJl#ZzTkJKT!+_7*mi(M%$#ufM-LY zoQ5D zK<#=1OsjCA1JPvEVncLum=KN5r^f&P2A*fP0nfw93taGA+u^^1=eX+< z86g#XZBz4#!UT{V20N86ov60}mt{mI`smh_O#yj99aA5T6dA2HV#2LZ?`LTz!x{kV zj%i71+pCO(^lSAX>B?8Y4MckeOy9zXwIXpG%JN?<`j<~4>PPM42K&cRe+*|o_aW&Y ziZW*FsUpl4l`OkXO$E0oD*{Juf+aKQUs}Ri z6rHzWJQxvn_*8^j<9F2m9JDlMIJ|L-TY}rlZ7e&N`XwNe4vL3wfPS_ZMtA6HYhMyr zfjS35ja%Uf`~+A)^@n-6K3{UY&K$Xo^jl&Dj^hCGg*C|o1G#S0X`m4TFdU2BJ#dEqDyBC(} zg$K#hEV|$~Sdaaw3uD=pr^;BYQmAuj{|OE-x6xDe16K>(!C*|k*(BrpA*zwi4*|gs z=rZdppqZ#?)wrey)0u;VP^u38bG9ya0=yd8j~U?DatAsxy@^bW8;>}ylsBqZhR+Mnyb#gsSF=Miz1^RNoDU0eq7Op)*PgB|T)j*LYdtPdtxt%jM(W zlz-_9zwIUD)C4Tkd{_(t9L;6Pq>$mf{e2QLGde38ozLho5`8UvTlNi%e#jFA-btpO zBp-Ie(nvYo*@k)0fWwv>|2iO>jTbV0#ya;`LmmE0ZO33RjwMML^}&jB0k^=HA5 zK09ojh`lugiitW1*aOlU)KHiq#^Du(<1ljC_j71{#{a|de`!m~lM^PD%QfXW`lTkM zuqLD?98AIrWy(+FsXcBO2%hRC+Jv)T!XYivOmD~to1(5VaYrCdQ`8v2h{PNeY)Mfu z`t(Ni25FD_9|mJ6iq`5#M!>EAAu-1O@u@>kZhiV$fLm?HADc^mQb{f?dZgeRZ)>Xr zyt$C1(VJx_yv zurwE<-GYxIKwuY;o?c`VxxWi@%mK}N$tn5V1MKS3lL={XEla^foYLG4yP3$A`&Su! zlcMjFw|w`3z=%j0kbWnhdH3NKHlIO2W4f-#2U!ITf$D6yqH{L#@xh@CME}#&pPfLK zwu}yqqBMrnTnGZ2a{46JmTv}g3_f`G5H1?|hPOLbARZumQJyMhjU%MSYhbcRARn@} z*mBiaEH!@p6e_XXi)m{aQkImOw%%Yiw1rjjH;vE2zk}^;mG&O?DrSPnKbC$Ut=_{h zaCuQ@L$?m|gdDO!fzQsFS)jerv<2e>G^As}SbWYGk zWOlF%MU6W(QWNd9mAuZyX3TiS-iQnUPmmE z%E-&b=eq*^ApQFC0e18;WwFsmp5>vaQ#UyJ$o=9V*rb|XcDQGziTet1V!2nuCt_(X z00N%K-4JXZHb;(`0Uh6#pJ;F0S-QPWi{Cf;58JC-kM`2gBXH~YU)37$3|D(^A`@#L zA8qdyK!7o;5QFxxF;=w)8*-6XrpLB-wP~-j-QG$ZzEOi+?Ts|;d0g%JOnZC4>Gk*u zOnaB;_PE1xFqOW7nD%JWjtqksS8Fg;#{UWOlIMXbFH68_Mn#_bXNPA*hlEh&?VK`3#u!ctq&q>Dgs@3p& zHS`a;nR}+5p9C8p=HZ- zOsspqb>4fXh*H!)0FrnnxQ(a#c6J zU0j29J^=LSXsb2=ZqOuqwS@9l8p`Mn;kMBl6GB9fMRMrj6R&~V793xUbOTp4 z_b~zJaV-J*u|Zo|^=f@B29A^03moIV6uo!z0bw+t-oX06hGP?QGwyN3X-=^b z?E;Q9DA{g3Bu1?3>;7XR^Yhk3&F=%avV5* zh!o=E1Ssq_9G@|9mmp5#<6~$XaGZjIe}j*|6GVVxH~bnno=Z5sJJ|)t1wb)3J}y|% zHXPL>fa9rXTi`eaRXFq}_$d7LZ{YdERc*ra&xij9c&`6i;5qJdf#;>CJMf(TjfQ78 zNFE!W!%WFdDvL!__+6Cg)y_&#IeEZQWfnG>oV`&BNk<}iLfb%dH{n!X|IM`&xRjtB1ulS%lIrj4X;U~Q{ z{0zS+D?mWW3cjeL;BfikCalaIZ=BqKmy177AOtns_NsdLIDLpw=rgSC8g$set`ZL4 ztyl9XhtbP)UZXf7!lnM(-ucpIz$?o|tl{J|r+jdsU4I`z_qOgvZj&c49Rz5F0(z0k z98VxegknR>Q~F$#@NjWgPV@1Xl6Yl&^ux%f^LjBP4Ue_)sx|)RtJRIMW5X#iPvB`( zX%Fm!7qsizED}mM;H_Yvi7MY??k!OAqa)FdJ>qcc-N(|s439LdS5K~Lf(^}pR-8e# zQ@|aF7g!KB2IT4-rD0OT`r*YPuvpCxj`YoYH7w>!#J(VQx5-sua&c|VwXxJCAFOMF zUiOLX_gT;&Z%px?ebG)`Sj zt{Bc!438qKng`S%DPe{pIX5_)HwQ$+b&*0>`1EMFK5|*9lfN_?u8Rbs;rhtDXt*x2 z2Vs{+Zv30^b}f|PJFdlnTDm#oPKl@kIjxVkZd!!VxHQUu*aYDTh<9)0wr(z0Nz$qp zQG<34T(c3xtFi~0b!Ko~QBcVRj=YOKd`Z0e7;ASyPTb!r3LGZbISZXG^LX9eoA8G6 zn|u~lsA0=?>jfWA2=zb$%!AL7#GaMD8CyzuDqzpvMmL$|5eC&47-7Ts=qNnuBPX{3 z59osbA_hVo1O?KEr+yGsn_*))drLUC=dImal}$1aE=4F_5c$P zb1r!(dju=IOGV~2shdpE_(V9=~o^q_v&o2Pl`pL0do^xrNL^Utja) z$dp$y%Y1@POQRJ%MhF4&P911y@^3|2^TWh4?So%3N#J zZb`5_FYmOLq8|S9SjX(O!qaiL$zHs3|06(vyUh4#&gB~q<95NvzFO7p+;HD|G!WEv~X zgh|yeoOwV=9+Y0=32Z|LB_W<;cO|T|6Vh5F)Yu6pw@7%#P8if8fo|z+BQVe;;Jv!C zGHLaKn5^cpd%rvKUGvzz&mFm@Oh0xX+ftc+?7lEIXUoU##i$<6-t(Zrbu2O`OoCFu zci(M&N8c~?G}MmBG(=ao#1#PUoj-w6|2?h@quE{K%IUD(?HgBQ-DU7(T&a~vV=F#q zrvv@)TLMebW%c;~eLB~U^-RJID1WT}pYh})j7p~J43e;>CQsl^6I+#MVr}es)=sdq z+X-{+gwtD;m}DpPZILj_PRMGJFu+bYr$s_fJ7IK-ghTCw@)ilZ!Mp|S?r4#)!A^LK z?A@V)s@L%yS%yEhJt6yzdB7VK=Pf=}6OsCvm`vBJq|f&@N4DXE`vXoDfqC-?4Y(VFbNABuB0bq;>iKlt!OID6f_4p+PX21JF#7)fCa)h{ZHN0w6< zp9g;ctW|6QZ-n#lV2o-iMCv|$q37Jr#JpL@R&IijUg?r&{_q?qT0)fjEgvcGb_42z}#V3ad=R}I-iMHXhCz0 z<|vjNzs<%=L3tm-|F9esC(pty|2fbBy?lGFEocrtS@&?4Svc_fj_$D6X%Bdf5(gR z)N2=UJ6LkX*W=PV=F{(Iw)f>OpZGaIwDcaB7qI%n1y#FM-c& z`4+k_w_)OERhy?@$|XbgyLXuZt21#0r`YAm<_xqCf<^^iGOK1R4mu>FZ{ zT`?P#@I0)lhUwm(n-C&+$=_kf8lkMR{Sj<;hs%+f84h9w?koF)pQ|Olc5%2#Jy&0@ znD{CX3AZPjd*_lL#V62fCy<7BErOi(UWvlE&kPf=D)I&dk5)&!VD;GemcBXh?uUtD z${WB!+yg|i7m43pX7knh$Y-wbELV7*EBp$=wtt)zUSigdZ7&{YozJnA%f5^IaMgPR zx2(h<%L4e2ThPMUp}9s=QlqXWamZ>?NU3Ku<*++75xj@zaw_stPa)%ipFx`|$Y-u| zR*;kLc1Y}_YRwj-BB<<0NBn$W37{RM0l1cp#jGaF4}~0C+C93G{1kjnEv<6k`PijV z_!P7ykNVuX4|#OtH5_tV@~F0KAM%KIHHMQnR}&plx8a92w%eX&J(Nc@C++hp$35U} zZOEff*Sh6V_Vzif8`4GO(XVLcBFuz3TG|kKwA)q&z~F(G>P92p)7z3qGjG#?N4NJO zkFG!tT!xv|QXXN4+9i)Z#PuVTho~p}69Claojc~(^}~!7!{j@mURxf;ar)thkAk*5 zdSo0~>Ui)Nxp~*u@7@|9CFPOPTfzVT@KvseSI7A&k0ck_e--mtnX5JTUm^17WB^SJ z*Z$zIYWtI%4&Bvj06V*@=k2qrm)=Tw#J+?@9Q6yc4NrjEzm!J8kISi7KZ+G)aXs%T2T8qua73}7I z*z!r5EgQYx@a5lD#`5I@EAqaFT_z5D3{w_ucwi_}g)-sX!(j&}Issjlz| zuJBQ=@CTzv_fdZWR%4$r7Rw@y2m?F|ugY*K`~hzUV)0aP0MD0-mmpjT!>J`70%d}G z3h>~Jxvj1$5e6fFD0?qJRZuxSq1jV4mGm2$&SNg|K}_IR7=k{N=n42cAUE_l?1AGb z<4(0clVW9~+9~AMdl3y`aJs<)0&X4cFpmtt;YvJb=~V|uOK6W@OgBDyG=#l}51a>> z!pXuvT0^n;Xi}B4iQh1|*%#;BA}j;%`#m5^KQ6Ar7C=6-%B}#Di*3cN39U zjQV`Kk`Jq0i87(WEZipL31mWJ_RQ)DRRWh^c>>JG>#(XGr$I{G>2bj!D!?Wdj@9D+ zHi$MrqX(n#d9HjuEE^fHeN&TI!A%n=O(ZJ|I@|*MGHa8l$#3gDkFe3`Y#V)zj27Tn zHEk2mOPY;@sb_MMa3OzQK`*Ro9u5(Te^T@u^2Un0=|2lGO` zB#K1I!c#RKm;lqSpp*TcqnLg*URUl)@Knt}5gS77A@^+voet3k2(<@gaM&6083UoG zxb@Nogg$_ni>wp%#mMVswGPx$5SpG)5IRl01I%JrB>+tdRt%C;Sdo!pAgcfX;|2zB z3&_9OFI*D+VK;OMw;E#q%VHuI8TpV|IN1#~v;gxS-LGg)k%7SPi2FI5Fe*z^&=XGR z4$+9^*{+A+Aj|vIFurjQaeTOci0F>@puza~nJb^SU=t;*)ps>(20=W;JB4>Hl`_vy zTnp&e!w%A=w}y1-Js7P8rHu*?PfAb4?g}9_kNpQO>3I0Rvr9k(ep|q$`TVtUNj^wg z)21?)^d;b}aA%PZX88xRgcI9;1)r^O@~23!mLObex6Z?Z^Z$U^XQ0~Y(32@(z` zzWy~`%5;C!zX7ty*R;7*Q)%e6c$`dl_E|I#EMAB?3-^BQa4?^6_KS!j`*@Pv2fts> zlnvWnp!=VP^A-@W7ZRJ9^vF!qDtw=8&EgF0ge%-6sJga%AwLRd_kRG>Wf+3Ry2II> zZ|4kdRJrmQgYFAugReD1_!d$o@{6uqplzU~HDtseBLdBZ zM)XowRYK5yA$Mw5ZkU1WDcY{i!}BSyM)i;P%;nX)xBkFcFFS-DwM5{0)^j^6jw0O8 z>Yy{)cvd$S|AKJsMILr@T;94LRWuJgb`FViL^72j2_J830jd z^55g+Pg7SRKPVS0g3qqr!~(BKfqcx-E#AJ1={|}^y1xy0o`E~Mfq$oV7W}&hQN%kh zo5b%(3>6LoeSv>tFDCvKrbQ+L0n`sSApsVjV5G*7*&-MYy8(fGQ^7bXn- zB9iSYyTO&GJ^Hy1{l|x+9ejKD@-KNKHl`#-1)7HldZJ+3&Ak}SYPz5l41)g9I8_w? zdqKrJ-vP={g6~1@f`+OM_<;J7uc&A9<*bA(b(1Mn>C6FP)T63h64UA+bR6=zD9 zsi=TP%={vLW%CQRk6H3EEf(t*Z$|<7`Z8ClB+V#yet7wq*rmLOYMLM{Zuy9iS%Zo(adnV72Y5FH<}qI4;e7U%saD;`Cx z;orfJR2oSQ^bA&-+I2qsF+wIquXnbsSRtaFC%ofwKK2m8ui>O#rQY zTV$JJ>#56Z0VhS;0InLKXF(ZnO1u_}6hZn!@~F8P%?gh!PbVJ*S>uY9X7$@s^vlsM zP*(6ut`pGK5~#l{LX<;B$=m`qSOHur;ZFk)TYY>u`lAIx>1$yIG~ z&3EUL59TU0xr$A$5_c~7V6Jmbt}K%)-t6@6IJ3%=N0|+OH}<*HpB?R8-@xNIqE6)h1Vp8DP4*>+-=|g(g?2 z$yMRbB_GVy&E#5ba&2(uk`LzEd7TDBmO+So_f*RVbFDGCs!gu>?p*T0TrWs2EF+NW zFw9Jd5OTg)5yJfi+^%qr*2^EqHh1H4bu(tp=G)g?=Ju?CGN0D$QMFQ}c6X~#h&Ct_S=?)PFhRq|J zV1>=@FG4EI5}{BwhTW&AzDRA8-5_W&z>y$V4S(&ryhgVuKtith?p*T0TuV%@VpCU%JC}Sg z*F2Le%jC*;=aLWRnrL$EpJV$?uKjcD<4vypbL=?DwO@|?&D9zV`{&sIHn~#F0Mp$A zln)N@PLr$D={G`UFVoWy+hRLci*b(dV4lQ_JX-nwKq zEqQSTq!B91E-%ph_>d*w$2IB~K*{FEC;1&dYPrHExcG5_D|~a2lmAXv_yt$E-W5L5 zRcBvU_r;feu>N(Vszu5)mZ7h=Q%v}@H>virL>*Ao=mV8^(-~^P`5S6 z4zIl$`00cb1FO2>N(ek)#HvEc?;wp!GU8dEnz3A*A%PjV@PZXWSt|nG*$DF=7Xs*% zKLL-5!s0Gbp{dFrzf<)RoK7rf*XUV~#`ua26zqJg3db@!9!|a;RC}GVFB=Xe55*7r z<`2K_tw4S73KhnDqtLP z>>9qB>vtX`w%6~piF2ehl&+qLrTOnjR6mb(ctG{J4oyE~!ZZjy?h1eG3O|Ez3;N?F zA8jtRM{~)EDcZ+Udknhaa^|NUn4Br!v#>c*ha)A>C-wJ|=Ez}JM!zGo#xY!hVVU=L zWIf)-gG{$#u-&oQ^eA%T&a=HUCSQPeuLl^TfL_f+`-a~KP7x@rTjRH_bc4Q{L4a8~Uv2UM=CKyz69dS-4!bCutp75ugG z-HO^sC-#C{URx0z{16=&7+ej*)u6b_4>v@vg?9(_=HO3UZT7~3x?mA*T*h;fj@&aP$n^j?6UAAVR;145YL4o56mz?X7FL;aOw#! z9~x&rT&zm(qpm_1Gy5h})DCQ4bQv+uc|X7t*ntrc5~u>{^N=3l{ZZ(5a4kkM5F@FH z{1Y+oe>CqEXGO{}vg*q3&cc8P)owX7xDq+fLC&qShj<49T9F%2K#jOUqasmmrKM4k z;ZG4Z{wQv%3*HEaZmn%vF5Q}(v`>u>KqSZDO8hX?D_!=7N|i5uOF_kw?*OSdPv8OM zEohjz0UrQWvGOe34hVzbI;TU5f|!ey)ruwk22^Er`TI3O(*IaIDTO6v@QX&s?g)@2 z<+xZ@AT> zUzSR_{|Nq;rqYCiBp$I$IMo^a0q>TI#fsQD0c`3po!Xf{DCIIRh7vbQ?@|?i*gv~e zI58~Q%~qFfnN_ohtYBH36&LN?!=c=|oRb&Jwtz?ETik~%3N0a-6yaW!2?Z51U~BAw zx%v)K@sV?o&Q($gnOS?ROCxU~M%xQ%Q(Ay2_sLHC!VocAn;28=@`4SKb|`}O#JGq; zurIDxpI-|5;<3PLTQ9n0ki!#aA>4xB;e4R2W9Sz=H;r@Y6XNb`@nq48bHq{&@!W7u z3P1;gHh7`hg_cN`q@_OrD$l}E5bm&(2fivlCBhfco%YV2-8F-E7ZgD+Njke>0B{P~ zaonUnKJb+%&>5L4_T)`Yu@=Q)tymHlmu`8U_+lb_2(R@fNx^VA9z#a6o^Pz+k zU+&sTPZv<2#PwMfeYf^P0Uyphg#aJVlI;X3?Tsd**v|p~WbgIZ?pS^~F>NHxf-T-? ze;&1x~Sq|t^yr}{D{7bPri+qJD zRP7iCA?_aFAjD&?@B)P09?RxDT0!u08%2JDmk1=26(m2!c2*3cSn%9C5?(vKmdP@6Xi_W>nSzH$Iv8Ah|tO z&1!l#?nT0n!Mp>6tE&iSw*Zs_6X+C$puWjjycBGGL)rr-g2iOFn*c-^j}{y61d#>^ zZtVA=KPLSfoYzz5Azfaw*L2MEfdSkD&vy{*$AOkJFa(gBp zkK`8h)Vb=ZWX3NcpaP|1^Gf&8XOu?3sH#aIa!d2hSY7 zupTbl)j!cQYY%b?)()t`Qbh^c0`7Vj0ggUbaB+(FXF>g}j}-0e2l!5W$_kAnm_au< z#{A=v=vZ()@8rVfQTsL!a|qhB+Ct0C1I{pF*Q=ybJGBiO3IU-)Z9CMto$l zU|CGL$tL|_rVneKex6Cch3Tgwy<$)I8P}tJgX@P~quU*cbipQQ9jg0ILfgh8f+3H| zu{Z;N6hsk&$PK$*g8Dyq@FHCJZT~YS-{tc>v0~zJLh*tmsB88}(EDca^vJW4JwPzH z@C;dqY9rMuYDz#qjb6Is;p#P(yp9O==06++>yqyvSO&t*`P=B}3}|O;CwL3U17w4B zE&NGcTu(0-!T65BYn;B&m||aO818W805iYD=GXy5y0of~J#)xv;41=NxMds<$dPaP zbHDe@q!|v}+@8BnkZRR#z`4pg)&nQEaS%LBYAaVQjG>TARq2#E#neSG^v zJ>^j}T4Sg>4hLOo8=@6lrk-aB*@X*dU->xHXXLoAMJ+R>mhnRA`4o$yc_nd?hyCcy zp1{kd>c0R>jt~9ALLcvJcs#Q*iMVuytMVXJ;8{2{4jM9Q_>!3{vvUgFupyycoZ|T= z;>3~LLv-AOciP#EVTCE4`BaHINtT7B|7IZ%bbZ7%s6+`v>dzYmw|#3a6)-42x#s*z^BOB;6RQ(X$9}lmJdZ>z<&g9`h&z#%H77&;jCbtWFHvet!n-U{_aX&Tk%!MpX zYA+aq-J8YejbK%S+Q?`wHPFfGtF$2ggmFfJjEvQBOA#00X*H z2dWJQ59W9zq%K(H3m2gJaNd`XfK^_=PKWb8WVjGvHT@!+W7b6$=GYt)PH3+123NS; z6+Yh;_PWBSA?(ag!7yL2xy4$`#X})K$w0C0QmaS}(br#Ye) z{|f0X)?4;6YZTMpVmi)uxzL&7`R?DiBkjbW|qAOXtNgc_8z-QpFy{vjn6a!mD-d%(q+<_?74BWI$ zItx#;(XT!dM%aD7<}g@BUmmS5kAcy3BCdYon>3T)w@Xcg$51x^&C_Te=s~N?V0l}xhPD>gyJd8jd7j=O`q%~gZw$2 zG3ktP9xA#G8C565p*ASd7vtu2O8*0>I`075Ky@BlE&4JqkK!dtI8-%CDz|FChqk<0 zKX1qBz%x*)cDQa(G){EBNxh61yIW7_lIkJE`T1nlNN=g(Ya_V@gFB#!(p|w*@R(`` zZdR2Dhi_6}%-;)^r!9^K^9PC+dyniKSs^WT*kT_0;Be3#BQeBG>6b2Ug^inI5Q5a+ zbsg}C7l;ay(7XlF83!>!h(OyO=g;+`k)2AZ(BWN5fX z?*SKT9qbN3wuKI+i-{UG3nh&^m63)Kv60LIiWu;X{X4#}phQoYIlA=}Q1X+Tu zSbUH!dmhTF!(>Tem`barem{p;OTKBtG&Hr?1|(pr20YR>EJtCSHZ0E?reV1YT!6eS zpJW=V&$?jQ1tkqESFivi0JsnJSu!NES@#X_gg`@tHjbCtU>x@!gYgKC4J+Uhf~@zxM@}ZwFxpI%+7U08?1vUFc}SG^1C;X{Pxq@-n{l3DJ2bCEb?5?i=N% zN+PW=&N@ffMRmVPJAYMh0IhKH(t!^8KX$POevwceSCT+h(12Ese8Cm!@&_qt#49=tZg*{w zMG6Z$`aplwss0q5exLI{z@BSjXNw81l|&cKSfLRyG?RiZwZ(smTx-CNLo{fXlkWg~ zz#?GA+M*Js><=`9Ow-8Sq^{15Md?d$Kx*G8-4R7?l)en}LX_SqVjE=FVub4pb*J)) zxW&=+IanUV*)o&ZCRRTsi7u>;^pHiUvwu_^X~YRA?tfb;o=m^DG;%D^wVLvP!k}y{ zXBeL7J5^AovxVIBRLLf>efQr^F?oytqkIeiHP^7>+KO%%nXD&}avM@uUoIqQyx$SSaL81HO* za`qpCbw6G~e1x}pY5D#KxHRN@r~ohz=PjO#HQ~u<9N~X4e4Gydli@Bpd>_Ldb@+CM zn^7b3PiJ^L!s;YUwpQrtBCq$f^@O@edM_uO?+Oobg~z(W6|QiZD?HZ~e%=-S*45Sk zSNvF4d@oo0y{_;xuJB-!zj2#AicYQ?9yK|&zM>Z);z`U2ffT`70&5PJssd{YAiVBe zE^J5ZB>?a(-DO8l;bP!2y%>O#+|w7v*wHg&U9iDsYn!s+Q|s8#Q*1l>v!G?$M*cPG zq9WWEGXX^y^hz7~Oysa@`So^fS)FLd87_92)S_+VS$gX@2qm6=P+}r~(bW7UNlmXw zaCAzW$d5;wHj#Hi$TpFKUy6ww_FZj4Efx!Tq>21u2wtKB#lei;q(uKmLG)Z0>gPt^ z2I03h;w?T4?Spg4KJ*ltmGh(4@kfys1+~jMPSY~Ct5R4H`sBB&ZyaJ;?N?2Snpr$m z9|-l4rNDg?VTZzSc?$N`^n|vwiw{6cjrTa~f7-{BjD38o^n&}$W!?;!$7?8x0m&@N z6^tW;ANd3gyq&kGOV8v$8I72%$g3!g{VHgL)X&bmL(k%iQ1TE* z4d->A0m59&Fzn;csOo&1I&m-Qi8e*nN48`-;bw$g8oIHM12vC(WS{<<`XzA4>jj)b z3=RWo(}DAXN9mu3!MAeY9D14Cfpa5y4BjDq%;msYhY!brGxbkYWxGWF#}1rB5?VWO zjt5ZSUh_J;hOW18;CzbF@$d`jpw8AAZ)C%v)O+xw9XPe}Z#!^eBg=Lz3@4rrnCS`r zI|t6+p?eV7EkN#na^QRz6d?Ty?%Za1P6W5v=D8%Xfj7hyunpi7yf$IG_u1Y+MZvyq zuT9#^9Iwqpe$hqhPoaibugww>9f=n{{D1V?+}e+WPJA5Y!LkS_OLd}12_+^Ia-rmt z1b~2PYFCNS4w^8ACno3&zoV?78Vz=kPoWZ~1vMVi9ZtMm>M)1sG3XqOBu)iHVQA^K zc^Yl29CD1;rtG7`|LITCZ`kqIOb41q_8?N71{$*sS(}e{sA-o8(_wC}D}3?^IuGL$ z5O(-Qj4vH8Mn-S8d!{G%AH(O@FmGIz&c_Idtm&S^laG>wSaN$7g>bZ&YiD{ztNTUg z?^g4Wi=J)-Ak_SkBD7{2p5K|}kyI_sR%LRm>{Aeni`11pL zy3=is==tajwYe<%LA%_d)strnZcUg++|pu@`;TF5o&p+7k%exM5-#AaKsDm)5N#Z_5#goC**Ev@D&Y z9u>Vl@iBrmlv*RurH}N#BtlbEI;QPDone+_VD)BB5YW_SkRhDd^+C>%#v}SjX9yS% z7v2d?7DJ4R*zz1|u^fF2wV8cq&|8zCw+hrjm%;^rZe$DTts&Q;w|uWdZ|9jX>FPpP z_%>H~7{U&{+4d(Ip9+GX!uTW(tP0&C+0~CAU?IXmX|EvcQtkrYFOaEE4bd;9@{i{7 zjxbBAKOwYnX$sA$tI=(`yJC$7aKj^5dBAjVn@|pBkw`@c;>1RLkjw}OK;Fw42K-o9 z)8PUnFvCjx!mucGNI3EzNgNuKKcSeg9?mG-O$7JYCUrMJgqi#Le$+=>uS16=i2i`l z`e4E}I^#o(2_?UcA3=DO8K;GsJ=xI460f|E-Q(iSNHjf2Gv9i7oWUas`e1@5Q_~iI zNl+@8%3C;b?L9)41`o%PENhSq4)ZL2G-c9g4?rwH1W9ude)$iiS;JrTD)1RM3{D?Y z0J#ZPaRp9rhp(-~h1&E(37rZ<@c?4Y&1rm@7OmypY8}1Hb{*4Cj1cMdHhs2sIABmmo9Obl5-vHO^zQnL|0Gp-rKndyMzng{=~bOpuY&NZ^8X0r#VD;%P|fShNI!S z$lb2+a##2Pgk_$hZb_A!@?FFQ=7fF0DqLyFYa@AlA^5v5_!&0@%euFz64zf_4>2FqKUP_5HA!;q^e zGn!>E)%YTF#Ri6l8dJK=Siz}yZLA=a@Gu?*c$#hTB}s5g^wj}jGByMAZP3-JZond2 zG3bslYkhi%72GOg`orR7C7~R}Qy*Z1Jh9-?zHR&XzRquOGvuVjFcvhczOvha?GmRe zz0nm|H(9B}?<{#yAvLjr%LiO~Ht`R&4qlIqb!s9I()c5?J$l7rTu~o>uyP)b-N-io zA#HMvn4;c-RfU_fvTV@{JpeLQ)-G7;*c&~7C~P=;`w+gU%IRe$dYMBBfNsh@cIwqw zwMgCUorPOO)amTugK9M{+s@GY_q>tiq_Jqzvura{e5$K%l^)) zlK!T<`a90_SL=60!Fv7ju0QwzPfWl8*%!+4g>vbP2P0c2Zw%zQM^0Mcm5+FI)Du{U zfGPlP@f7mCH92+Vrx!FNc;ll=J%Q}@3g+oHy-mHrM$UpU8s3S0aQ={Vkp#?#`Q^X(%tg^j^y2^-vhI6>W;6pPwZ&HFnAZ@&D9eokl;x&CuYMky92a;!xGrQLy z^k;fc85hS;XgZFCc}{c;I{*T8pp>D-w}@&&)Mf@>^Z(~;m^f_Vj~h3;LXng$!d zT`+(W4dBtN8}H_ zhqlH!_zo(hvG$zK=N3M)s#sQ2oQs$_j!F ztSCvPq6jG<2U`WS0e^NHxnwy+^G@{uI1tZgV#jLwD2U^B@h3qv_0QOM)Rg${XSsR!nwZJuSRSEJFa2q>E}+MUS)po}tT zPZXIz|J@_o>I6k168)$QZK6|aNTA*9!De?V#OG-C2+|WQYlS+-Y4_)BwtFf^`vMH# z0+uFK-DX4CuK=iQs#b<4bhP$mO-FpT-V>mCOFc0SbkwCK9c>35sztpWI=c8shmOv0 zg@?MrBM|=I>Fe12lfZH4)c>8n>Y<f>m89I@}$qllqf#z z2geW@54RMdmoVHmYawz*Ym#yc5%G8wUnjQ^F?=nY7&(Ku<&q;)vpKOba-{e1QMyV} zTkjA#G9{PUM2;-6le5o5S=HPlR*o!10U{&!tD-bng=W>2b8Iv{ngttB5E&gga!5P& zE%mL}@Dr6IrL2x}1f)Val69&EdQ^^lDkB2545FyTrK7;o_bx%9)^dbN>AHKABfsko zfNXxGh&WAuQjYB5(Kn8Ei^`AE*1rG(RV$e^{=ev!BV~gBHv|8#VQAHf4!XbE#XaM@fj(t^Lvj58CQF*no zck#7ERHO0t1IUOvBO{2wX+&U%h*lU3F`>RXIf}srkP2<+L2jwAzD=nhdS~JeiUGG= zc&QaR2FAG?ZyH0)q8sW1k%>ZR56ATQqcUrdFD|YJr3CS!z0) z5AKBCqza{FtX*u;un{$jXt?AQ5e?LgzP-4uPCzs;u@5REHR6Ir(p)=xyxG4}N*1z5 zwrDuOY4^R8IpT7Rcp7BfHnrzhGvp>!+GcCncLKOf zeGTr1GlBcaUkVvRocm_5CV+7+1&y9V781$ld(R{dG?AdT^>XT))yveEhx$xacePm+ zcATWD9qRiko1A8&H){Jl^I7H{uEz2nBi&5M)y(~Nx7TJwo9Wp`GZb=IV`GcuTQyah zksjx}syf21D&$nP;i5JMuPtD9KXr z&wvbz;_ncrM1~4POMHSl)l()79e&p-v0lI+tI%|HQM5#_f1yO>(QHzrXz^a9!Gr7@2+FT{Cxl*z4_6o9IwFG>Wzn{hbX%4^ zfZLt6JZ-h5C-e$5g9$x~rSybOwBgGM{pn1Ee5)v0Io1+DvuesSU717^|4Y+_o^V;t!->0n3t zT;mFlc7-o;g$KF9XSl*=y26*Z!g;Q6A6K}CD|`gPE)gX10(%`$w)i?VHVE$G!7h(V%2$w{ zGPJeHwlHxj7T_fv;z@?>FIF<1It+-gfov3ovH))K?m_44nTRpw0ApZEXi^VU2*v=} z=AH~({fCXZP+QbAqzNOeS7%3C>!c}W269Vl!zl!GL*Ep&-4GVMO)4weP;WN$>q(kM zf%`8+x>u&+4nqeC~i=ga?ch;KQc#@-ie6HCHk=S+496tWME@a3*8)J>Otyv^m`N#v;eu5Zjf>T_;a zu@95~4HL07N%E1RC?8pHgy3C4OFq&{t-O`2B&t>#tyEXF13EMCGk#P)Nc%*~P*Csg_TOT++S#w+ zHv1l6B>V@s_8lCH>1Lw|^3OT&B^UAhm8cUGZxfFwhORsp_|1x8+`U_%(&R=KLIpZP*&hAg27UR^&tp5OT* zlcKAf@*`nIhai&W4Hh=nqgVkKAy#99>J5~ph~*a5#~o}K)kPk6g`Y3>EEE@8J&P?XY*{!s*DmrARt0VDkNU&Uh{MDVSU<5l^4H+dtSiVf z!jJLFFxd9O#W)Xz(xEEGOQ!W!NFE!rddj0>XrMi}D90n0KlBvc4Xn^hHTd!8{#16d zJzqZH40;w^@2gx6%#H1!I*EmIe=1+cr7swi;lFafi@sXG+((6?=ivW&rR*ioFeeC$ ztD}luor4>Bgr zyw+yr)=gXo5=wHL2cqYDtx)wif+BGW|Arr&jplIY5{$NZ&&9x{itYzicxsNt%jSPp zR^N7U=H?-8a)R$tCwHoE-ar%tH;()>!A86K$iEOa``z}%556ET;0axyWc9l~rK}9k zAcRMy$pEY{9_k8?!jw(IyISS>!GUm}*`^)^V=Tbfa0M8T=LZ3En7ptvGc7BK$v}=< zki!ZOgnI@Zp2O3ia1R7b=(dB@(MTQPs~nhAY74ay*w{n=&1Yl+5Xz>XZl?3(EhixC z@KYm?*?F+l(DgVHbiKv?<6$PJ|Nh{Fmi_Eg$6S_%pyzQm7PMqIbo9BtyoH(%m0NHEm{Bwmlp5u}Ggrz2o&*ux`1Z92qJGFHYr z)cUC_ya8dGAG_fV^vA}sKXgr!zvqbrGtAVDT;Qf#wR_rgg`44mGQ>MT94?HfIioB*5#7sDf0N!E&i=r)1*RaqL2X5&HRGPJc3 z7m{-~*1z1@akL@%t+4)7184c3J!jindynjq&9#as&_} zojVY8Pzg7er4SJ@;wCF|i|T{gMK!VqNgzkzndhn6f$rgAlqwA>!1M@4z*;hlB?YEH zr)dd%kQ~$(?d1y!{0B4pEwBgGkHB%A_A;6a8Nsbf z=!t3=V9nRb(qJ9)Wnw})^VsPknETD#F89njOYje?2(!P6xj?XqZzSOaugtaQkSx`S z(}sGPjHy1s3Ia`WbTt>o@UkO$seqp3EOlrnJzbCiy!3)QDSeR~H z@CAt;ooZOy%tcK+ z?DO%6I2`kmwt~0OU;?+6f8h&t@lgfB3$DRht9EyL>_R6^I@uRGjgGKDW1}ClKB)`! zBrIEzFq5gQ*UGJ({5@w4G)AwUP}{Um?+eStrUf@z5^95;XZeAsko%pNX)vS=XxQb01{JsoLqJ3R z+FkhDxdAn-nY;*p-}THqA4v!;^UNF|nTCHVR|lD`5WCj$=biTPHa6}8T#h%x8$DHc z(i#*A_RpzI5o(B{id& zPl792@yp=7NqAZ-0DhT4c$ekxlJJWBt^5_%bO#f2SC@YtnI#^LL$Dl+hu_x_xM0(M zD2Q!WVg_fQXfL}=FdC)TMRH(^WqX*JI=P}DawU|;vDJM(j*Mi$#xL;1B=SF!fTwC9 zTyr9cXgHeAWB66)0IrdpNaKBHpc%&9$heO|vYI}5XeW)Y1pgRm&y05cQ5QPAA3U(n z<_KF5ID~TO3Krl^j8^#KKdsz9JTrF_3UmzxhuY=K+*K5|_5gXU+Rg3#;eqkzhdT=| zEz0}^$FRHjbH6I^+*gNc3Osi&8<_N^CvY8V^HuKd;F-x~4nkcf4aZ)l6+VBfuQt-o zSNk2VrrOQ+aSk>Y|C5wbtjY+GrSzkt@TcY|&mq=U&mjen6t(5+{3u-c-Jkw&Cvst& z{!Z}tGvD^gJ}##?u7T0u+$Z}pcwxk*E?X`H+5I-JEE_4 zzJXYhA`Eqc7s>&})PMu%Ap~B$Vvt?%fTF4&Z%S0}9)yFsJ&@cMlMoI`yHDf;P=W9X zY!PD(YE{-I1Pg$s@0R}>`IYxTg!9xnFnR5PSWu@Wp}KJ1=ZyG;aw44f9>d@2@KT09 zMwr?f^rgszU5;7c5fhGlXa{!Kf#2*v+HR-pQ6?N&XHr>V2Rr8+1YG7+vp*j7GS~J> zmlhbcOjhQly>@h5T=_{>sFnTkA{r4|U682xQE*S6kC&VQxMR#8y?Qk4(}!aGa44cz zRUj6)b^paQ_BlwKZOWteCfaz%QI-T+%$&;VbLX2z*FZif9_|VS>H(c>`BZH zJ-t}`Q>^x>S4w#r8soR9G!$COSyp=!aQ>A~_9ku`)-7&nG2&c_G3I+=3N^wLX<`i_m|27;ivZKVCXGc z8@Qr@wPSLc)o*gTCs2nm_=CqoP@xt?qK154U?{5#zyqs`XQeQqo7?0G^rvwEM4TmQ zcOWg2i75L#J!Ta2ad(-_>}6g(h;z9B2&L{> zfGG)OZ{iPz)>oDVbbc9{3ZwAQ0}0?J6-G2p{HEb`(bpk?B46%^-UmRPsuE9aFUE2> z0{=+(|0c4~8r0#r7Pen39%_I@*IWZ)`90ntkXfih6Ha~P$7r}N@_;M+swEL90}V_YxGMc6 zejA>MqwjcHAT$()gL2^Q8Y}ZxDwO5PjfEnwsVbJ`Sp9l6l>chYv%>uwtng%L_S>+V znYm5<0#KtQCT9zLtMMEAPPjjKc?uY3SyyV9K`(}13d}4`FFPZOm52boxrWb^7in5n z00(s7xL=u-h*bNU<>vHV2;c+*r^oLF+JWR!ePHPw( z(s+*6PavcI1`glH{T&>V+JwXL`-a02E;!g@ehb^Nji0Lo8ME6yYFoj>Gg9zK`EV@d zb7F>`0q!;SAK+fW$xtJ*&-#KS173B`RxCQxtzeWe&eqRL(~h z{zH&<5dK4OfetZ9D>VNhSR(-mu5^N{o!}QvaDx+6PH?Le++~6*qJ}FB)hg6P3P;Goao`#8t2_M*pnVKMFsp*Iog19_FqgKPg^nNkT1F5LTG{bcEQEUC4@##Bq7a7>vsa zrkG$uCAMuPv8$7qV}h^`q@V|IL61|5{$iV{>n`mjhi`-G$EF}YFAg<7`w zP#w1lc^NkqaRcaLx>3~tw*379Q4m^PA+$PEXkEL_;YV!GYqz%^=MvO4y1mO??ftu( zuDiRdy-Q4c9TBH}WG*z#U9Fq*qB(j!ME?2}8su+)iY?@>*=h^0`p7IK+VP`Z@mILQ zFSx?bxWa|5vO!n)PK0CmZJ!+lL;T^ZlC%LG;xC5tK(9%Be<&xk1HttG#sM#ufenRO zaZ5`$>9h85O5q+3`%b;eVc6&y5A~)@X&moQwZca}ETvM|af(@G(q%0_Fnlkj;??0Y0PGHmNu=Iq3d!@ihn{?s)E!%d0JA zi=pi_ul6k2+PFaFpjlOkKel>*iE9!@xx&2>c1wTze1zElfy18Xj*g>OQj%w3V&k(% z#|3I|{<3m87^-$5Di~b{UcCk>)U|Fvi*Q@{mfK^TqD&ztu-?QK$e|{#j5pJxb`#Hk z#(3$cc{`R=a#>1h#yC_I&RcacBYJ<#Lhn$OMBU{adTh%a9JeiAY0?f;R20jpj$Lny)%oZ9jLL8&IS z33S0@e{t$EXp3gQO6v#M>bZ3jt9Bu4z)KEBJ@Q&_hsKn#)#@k|tISF8RK1EyLR0(d zYk<%~JLo-Fqm+FETrOTwn8ijK4_1xP&?NtC*d&o(JrC$H-%l$Fp(Jxmx)B$r8cy@& zt;^XLqxFCzmk;nP9N_KH{~}LdE)r&R>+kaf>d{Dlzo+Uuw1zejAJ_OG*TemXc&Z>J z7)dK{W0kEpfj&Ad-i8za?fNbN+Aln%5$^rN-Jp)1$cp$CM`%_3G0tB2ZFziH9g71) zyp=79Wkav)7l|>oH_K{4XR^Wpc~G0H5jmcnA`M|pF=Qu6x=Fo_+#rxnAP`#XiIQvE-;e*07{tg}kc4&CKhPPhw$J5~?w2h1q}{R7a^zI-y7r#=z)5#l^c%DmWs zXi|p?MX=lL&c+Jq2SXr7=?*1fg3hP#nzD%#6bD5=xTPVGz^TFXLwysC`z4f$497$Q z&w#XXkt0!tvV6Am<`!?~#aHY8wr78*<2r3I)Z1X^cJuBxs9^TK>`rpo*7$WKI&Fpi z4xYdLrr|k~RETELeN@{Vd9xA1@G3+CMIzs#bLvY#o)?l=NDs07LZi>`=p-ObGEY|) z7zDtQ(m_LQm5+;dyhRC?o!6OhA{>s*0Qc4l^wjzTC_2P%s3W3zq3L{7ueS>*A3g({ zAZFJ{qp2U&MDu@j0`ouV-Jnw{QIj;VQPm=aW#R;Ysr}F$;kr%g8K$7t^{`CYZ1l5V z9Kn7)25rlZPhIg%uJ~VF@gZ0IZdd$n6HnIM&lR8TjZr0)B&8-;6sYa!MhT`hd3S% z1=Qa>N8D-j?ihTypDF0sN3bL6d9RO5i2}Pma)B%SgA4GlxWWg! z^8DnAzseQPMz}3~L(9*7PFBT?jJWdSN!y$}$T@8TDYqcwIKR=AJ@+FZg#pAQbvee; z*s)r@0D;ZJ7xm+z8%efkjcMV zPwgi*hHO+@kBgbWYtS9j##*Le0uROmN%jP;aRGj9#3tzaNPk!S#jdb>qNcdwYg}c& zMA&7`HToh|#4Hz)Vc7)r?;ojD3p=Ri<@!$OU6qR{c;j$gH~{Y~tjg1&*kz5X`pS=CUp-k?&FC1<9T&;)vvZLTOa9xu2nnq$2E%#l*MZozrdk-zUYXDJ-*;AMk*qQCGNNp}!X1)M3;44I8S%nPA=j4} z8%}4zFQa)i4Y3es-G-w)eMD0DDBYn}<;`hvWgVHAzP0kic-ZG?;&srsSBu4@*7Kz< z7**53jy%m5rBP|~G%i<{Z*^cW5Yu4vF&V${*WoEM8FY)u;6tB36TMSZx8JBRo(fk) zv5JJo<@T>1}6+?Z$jc6q5 z`&r~z$L-xBP-UsjL*-{@TcA=! z+;UW^f>x~xf?AX+6{;M$@6Ts?e=}Y^Y`)lX|qY5WHK|EOeQml@1f<1Yy8e&nC?IF z1v7N^9Yt>b%q@Zl6!9K2d{My+ue1P}@*&?1WGOSeQVvB**O^jgc%|GXNa^dQlo?*> zT(|UoQ_2jlG=*D}?2n2kuuuN*N=i-1aF+0gSF*#D^k)fwcqN69(rm38OZdYpS#L^w zWfe>!e}!-1r-|#KTRv6Eog_mb2jJ`Q%uyWdv=tNi$C)OIXZcz`qte3#yCuVfGb(u#CSw9HU)kXQ) z?-!qjtBz|w`p@$$atEcbK9`{j!fOGi0AhQM_nHL6_WSTBBQ#(7mr`cqAH??We&<&7 zwijaiC(ho`Zi?;0V4LH#fD>U0zteFa#8862+g|;K%loFE|9QE+UdwIi1ajM7RwHuT z?SXO|y%@Ruy^-5kC8k2=@An1I1c31B$!-ZsC$9H97Zi!}(Xl%F_98d`{w)AkifAao z7wj^kWHscqN<{EWxeIW$*HbSHgM0l0#U+AG!o$`(aP`a%0Y; zCMRdrKkc!ZQ++B2LyXtDDD)ZRas$IL>K;*BU+-L$$juo7BLutje zcWJ6HTklH>aL+e(j~JqQuan-BU}KY7Ej3|wty9k<+j})71Q!soNX(MlXZ012PHIr< zOEb4gGkWW2G(@zL`x5pr_&XcpaDnB9sfZAsMhKm(EHuJ8D|-_*^G=J`|^u~-fm8TCS60VaC z_W-FXW1T748d4q5z{*3>Gk0-98z=R=*SYtS`j<<6O)7M7vb3~~D9A;Lwc)Oi61n2( zc$T2mt0K|;TKK!@9_?Byoo%HLveFwiMe9FrrHiff8CLxdtn_#*?O5q7E4^@Yw0*}) zUuUKJSm^_;^jB6p&sk~q8%Mt{tI?Bz8kRpP6O$zxuX8a@U%H=$`_B2u*q_+sFjj3? ze)w951~`A?eE7&icyxa|+_r0mbePQ0_D=MRjP_@K~Y% zsKgKKoO6wFrBEGCDT30@oDgmCBX1FpNO|cpa&V}h_B+hWL5O(2Ge@i_a0Caj=46zj zn)^_Ycx>YtR-MPUg6`3Eam$vxwuG+dvOa{W)S(-~J^C>+xR^VspC!VOvpQEeM@?_9 zZ8lr*1>I%!OfPtjka`ByHr**+M4n!P`ZNzXIei_RT0Od*8{_|Ea-Q~`uEkFLPEgzB z+=GuDl8J+lD@?7qrj`gGx0aShsFh`Em9kd$<_7Pjb&}FJu3mdLX)r2H2kWH?bN(Vf z=+pH%YGL1#gr!&bGB12iMmE^TSN;<=M!0?Q-}hj;I9W4mh?II2RC+C=zbLN4MUYm` z=u=!5xYdKq1eGIvEWVtjh}jwhvcXYItwv{X#g}ziq%3{RV7CF&?51C3S2T*bi8;qA z@PES!#*mwjdD817tv{J|7!xRpE9>Oc68DQ2V|v%Cij%l7CH3#w1^fJ~as#0QgrNt9 zM*0yjmJs1Ra#8ydh7^eCA`iBDl4VS-0HS8u3V`fO15x#vCrl3%V&ba-;Vq@9RAeCCOO;} zd=oh4x#bShk?d81nqaTzF>ZA`s86wn6WGYwucM1lt(C6$#>`U>KAN_AE-I<3eJvo+I3HLXjVGji-0$3+EGr)bS5taz+4E3P z!Xt9?Aa}&`TX5ezhT2)3Itr?`>z#4g5jw98@&@R8W(&Y{tTu`oIh`&_ z9*G}hau=ZdYuBl09DaJ=(S*clwMb-YueoWLzG@GqArPZ1@a8N z=7NowwFp_7UiU^G`^u)*b);7|s!@JKZbF$PZbt&vSlatseJT$+<;}^wcV{Qf(P);z zijB96J^oE(4bKfPmPvII)ez8YBI#Ao9`qt{d3|1=5C{-?~G7Bi0z}SDoqz&S*%R4V3w&M+MDA{Rq;Fe`U#W@O_UCN!*a++_6rj z91Krs#x(If-=^!O^aJ_6o#+7@Z;8_NZt;7oSC4Lu()AP6k;>p2grBST5oYf#Ny7v4sZn|=@J7=LN#$q>dLk)Tk@tqnqNY!I9) zsgC0ei1#Y#(;6P&19=I!VIrNnX|lQm9HxHz26LA#WZTp#S+DSscYiwCz_PXUb6^_u zg=eg5JwT+*O8*tZtJIsIf(zn_08#T_G!|Lk2{;V0(}FQ5vYV$=OEPjve)#5Nmtj;M zAc=YnSc%>QNTtiAP3oL2Kw$W!+9gB1numbl5 zxMM)xu}DLj{}>8^4)R;&vTr}L0^uq0j>QLLE+=UYwqrU1f{GUPva-sqZU^cSEaB$f z%qLJ^-q?kz%uVr_%|sbC09b<9AZ8O>S1{C`yVNOQfW;lyzjGZP)8TwI3U)bY&luPp z+$mtOts`7QG$s0{%^+)?;CmQ(q&00O|L2?D4k9!R1Azl+e38cUJqB)y8SnKuJ@ z{JKDO0GvdNpn`}Gw|zqvFCxWBTFJDF8*ki8Aq)LHytCepht&a`^3bO&(d3OXOE>J7 z$N}c|lDy$E$+`Z7C#08s)Jk;$Wg`(N-D(7&cG>CZ&!XI4_ooxzSO9XKvxw3X7B^nC?8;QA3J}W)llVS zRFU~bsdCi=Xe`!$pXfTgdT=v)cPKWs=EhL~nmT0%_M)Q_xu|m(_kIJb z7Lb)v(Tj}ZWW9~ME(6Xu$bHzCpukj|p!2^#KJy&rnFnRtey8(pXC6A__m5R;7lZ4O zCvIH0%bxkef6#TOfZ5HIc;E{nI%|k28NqpWm2Tx7)7w#Oxv$P^xl^~$hIu(U?-Y}_ zn|eC9KLJo+zolN7wI99zQhX4fpa& z2R}w1z`)V(|-#s-zpy zS)!b7hfDVWHE@?tkez02588!0xa~~W?Q~Y307%y_B@KqA#8CfUv}iRjw3K8vHnQ0I zR@VK$X-Z#!QAHwHxzq`OkSoeuAv@F$2(FZo;B3k@EQq=4;n`3oz-C+c>1K5(kW1-a z2&B#Yg?V_hVYj+DQJalRrJYnCYKxSwQx{9gTN#xo!In}s?Yq@c57B7)O2!sl|FFlo z^uyJ?P92B(X!|v3LHyo2Fb5^)0(p&K9cIHCc~oGXzgfe2<1tZKzuWG?T9pt3>j=~~ zeZMFr4PhM*XHW~)agTYh-j4bnGdii;q$Pp*YL)=zM?WLXuVj?UI1PSB3(R0#1ox>d z4f8%sc_MNluFyAEj-;Llm;o8l?i0u~+TRrEAZXuH|Eq@*SfgYZ^=icf1XvFPs9Z9e z0Pkn(Yoxx1_RFF}e+?z5n;9L#9OhsMpMToK5K8PJ-1>;1{Y*22KhmQ^xS`e?Lca8^ zhfs%kW;iEHvNZ=rhfsfi%n<&1)Eh#Q)c1xkPdXLOsogI_7$-SGg|C@|A+*B~+!gZv z58YL9?))QC45sR(i0NzQRhsV7}!#x!g)`j;7&_dBjL~V=l6~*lnVXDfZW zl`gQ-*I4OWksgWr<#9AynRiJbw2M!?;h+vqOHfAq;q$QRINef(vG_y!nV3R^S;IZ2 z#ck=ng$MLK27c>YYeMS=_N;_!9^BK9cI{oh@^7SQ$9oj;Qiw{vg^+Gk-(iD;DRN9= zAp4i`J$ws09@+OwU->9hE!}&QFZd4UZ&_6fJS277A^m&{Tb*p`-uiidw`xS$`|cLU zPWS+2NBq8X*5P^#@vEXaFCB7?HolbnLek`B3HCotF|h8k;!m!f%E@oUh#_DYf&to|aBNr2ZJcGaBCbFlR#j+SkIpVP;U` zV$v{A7R?CGHrXDC8JRB0Aj-JdKpxhp>H}ywBl5Gcg0EsrKxDe8-iNKmI*5mW@OdsO zVSmRj9*p?{e&yB|HLLK&k;uYmA}#RcyEwcyfWVg*o@#$#S@jnDG+i_&VZBU zBzj#n^)u#wsPlX1{I`*>=B}x&53R=?NRiv|xhtyXT2u5{D_w-Ni7$~kGr+fWzmA*7 zr54)5v1~i+5F8Kog9R~DXB6K-Bm%I~XA)1f*P8t&CJ;Oe3-UwQ*<42hdT~bAeydug z!%~Tx(tN=-v0RGSZsiMZLkorx3S2Jr!(2#~RkV9{&38uNO#S--`oZW!p`O#^CHjRnEp;BEJg_Bm*=+AxixD&TXKi3 zF_D9jL3zVjo_8twKI)0~33~b>u>v+&qTuhcp=W@ApGSG|a z71j=fK-8m?fqojg4Ej zQ9Y|uA)`NivIhJP*%C_>pWM)?X}cwcHtZ-to%<2@7TPFNaS(F!g+iENxF}8Sd!I8< zE-gwc?VV6kniuxr(x>tgPMs0e{_JXB`DmugCiwyGq+DO{a{Tlygm)Ps_T7CheoDKX zv#&{Ww19!KY|vMZhXQZ|U#Id-f(<^5Ml7SJcc+p$=LZp{buBy&p2(e+nF&s|Sa4TEK zgE;)v>kOQ&;R5_V<~nEFI`vN~)?8jR0x2+%f>TvYN=%5%K!d6jOz5r(yH-V+YqypD z8)?gf0{RqqEBSqww?vs57Q<2SicqUSB$9!)w!7bjq6)vmBe^s~?wAXgF9#S~K^2YF$KY-DIfsIFonAl?E5xUX$N7X)v2Q@36 zISkdEtoQ3N-`nCjam?2P@YHYx?j>RVIaO z`*mDoke&1a_V#@lVm%F%j@(y`sIvPjqvY<7443q0Tj^m)TjbXIK;)c+773O;FH!?R z*ywyi&Ngtf9#}$0)=*U_G$Bm5=o0s>jm^j6Xmd+YB5Ww`f_l@Aa;)1y9ZiIzO&{DJ z5o4gP2irr3Y{`YIkelQ6{7cI^*UnH?erSm< z!lB5w_%1vkW322OYtVBF*i{|+F=ldD*Mf$ptvlM@1lwxw_ecrzz;hp{ySjV}d!g+$ zP0GP5B3&s5B!{K--{;o<$tWBB~C|jAB=Udn}LtTs205%{T)$5JD)-RU! zKvhz71XmAedH~kK(5JrO_@*R>EIn=)h$h{N) z)#K;XMFT=x$}1<%$E9^>5f=pQDtQh2jQ~TosA`ANH39DJ3;xVug#A#r3(_&=KvUJJ z&3<&?oAGgUggfgK;ZZ{ve8D~74KAGzugLFnT}B$hMUz*|zzwXv;8hsqvRuHVI=;)* z6KAdy)BO&vWB_Jy%WFDgtsvD7IAlLm557YS+QYXP8xUT}ovyfF^p|Ne;yxhaxqLRd zi>^kFkFaEhGCk4w)xH|Tn+eb$)UA3k=`V2nyln2{n;CeFAvcC z$ivkIn_QB8D`DMc=>M=THV^Q=l8SZeXXLm6>%l6nb#>}X6KsM)u2zUj5e z+HLkNqL^%nFbkN+_woBsi|8v;LLBS&4MwjeL^H%_X6 zpRszS1BeBjx?Jv5%1;DZOJ9FDXZeLkf@L;v^-LXrp5)j_bzh{1DQh2vMT5m_(n6N! z1o5%D4>B-v*-|X|p8rGu3k2v2U9o8I(WfT`LJMYmgSY7syJ2i{P zi#0~n+9xnpJrzT5qXk~*gJ~Fo&qWv-Z1x6Ua6W4246Iv7V3J}P%=ImV_Xn=!?Tj}n zoZjdFXX+`eD9;ZkBe`q|+DenY|{|8VLJ$-XaaL0rR-CHy){RQ zpI5*sXWP80GN^4jmaqOO^C&L8gSlcLT>R88Lqy!4;O!iCO{aCFYv=vqI-Wl=dh;*SgNDH z&-h`*edHcF+(V0Ej4CZUs-75C`V|-uW)t=|e&^0ib>jDl3AJyz@G`%n-l?v0IuF!K*94T6*oBNrjQueR{Fbb^0mVS z&08q`u?lb-c{k%Lz(u}bHzpvLrr}$zsc4=5ORNrrS_eWmgY?z;xNBAOgCo4qDx1 zsuZzSb)dDX_*EKA-l)ZvP}^5}WiSlAPF)BML40$xZ3%nb zj&d8KuY5bMipMg*o~*$MpDvLDYjrGStC%C_9gOyuWE>1Gfsc9RQgoM~24cWcx*Uh| z)WH}U1+T(wOSs&QPtOk@i$zqdTU^DRE(P!_RCREN_B%mwfW+Aw!fwp22JhCaR(U`2 zq$-$E#HLW{r1oFQB~tc1!&u`esew=vsR7cXi8^&4a`T;MGct)bC-pmAD`f;>-4ASW zi8%;optky!Ss0G49L&h&M__tfa%)_;C5KX~-5ZorOQbi##rQ5(E%d5N4y$f^T~{UO ztXgWS7I;-9hgBDwss*NMp{WXW%K#ubtUAk7%`{bWP1Rhls^qZh^{k2&j8x(h@Fyly!7J{^zs;{!p>RleYAzSB0%-yZ$!>LaEAHlB$taTg zh>;TQ@3HTRRfHBW*GYYT1y_HZEN*6u!CZ331gQbVTfW1YEPRvEo8CiW1FJ4E71pBy zxMnh%5KS480|M13R_bXgrEu6-(KAuSv{Q;+SmoDZ3=_G~S#eP!zTT|<-U{e+)-Zkr zIk8!-LOm%*19fTxtB_~eJD0Lw)pKmfr8TsULalYo70d&_Nv~N1MtOlQFLld*dR3PT zsZgG)%RTy3n{pWoD3C6sG+Pvx@RCVtK5A+cJ*3wb!Q%j2x8YWm99A7`stT%6wNk3; zmnyv4lF8a>YM1h_TT6bh*3Yl#p%<81g?26Z!CEU# ztsSOTf*=6T*?J914y!)qHpnW#rC;N+(8cxbrd*~l%F_(4W%}d$%UBLnQ0J6|(0NEU z{EUwQcX1uYMsN#C`HyMpn*7Jvolg4(KUEiC=Ytz_--Im-Uh2N!Z|nebDp^hVizRck z83E=13g7&&K7FS(VgrA3$vn9gJ|R_*!N$ARo@DG$f$*h!xT6V#&e;Pw76^3;IG19L z)W)7b=qs$B#RF*;yyGS4QV#A6#r$DAL$d2imXou)5}0)LQiKy*cPjQm0|>uCBPELk zW;t|HQ{kvZHctT+)X``djZv$@PEzlwV9fOedt;z@++a_@xjGq8q31c;6ccLATW1g9 z&F650>GqYn>18y*`%KYhCfi*1B5;qkh*7kK8_IpbwIp1){~o_{5t>2_W`B%R+f4Ga z_ZGLy?*{R(nZg_`c1DZ&q4B7k)ul|f6-2DiV}AWU6m7#g+xjvn|JRz0LOeFO5W|Cb zYI(W%FtvTx^f|b zXh=&o&cy({`6@p+UnBE$Eix5rrQ5aS2WwTK7S5uJaN`d9#|??M04qUbU{}Q$*1(=? z^Jt|2c?LMW65c%qkjUEyLq=fX*`?C4ZJ^BnBgGLbdMVB6Ma*LJ;wxW(%7L=|xxVQG zf!5qztIqb7_d!bDgaOZtui||a741+>b;7vxVum&3eTa5xaUc-tGF#+UW8VTFK6WWQ zJX%VOP%fII{NTg`!>W@3MdSkPDm{Huiu{l`YuM2tD5szhf#3=VXVRX?2htqQ4+lHN znU#_LT4lv=&f3UeDJ?|{Tpa`GpGJi|5Sa$P^`dw#ksi3bSE8D_u5KSJCR#$E@{FV- z@Rhm=5zOCl9(*n1n9_W+nmy$?GT7tm1{s)Jd=^>WZZ``kqo#{{YGtkT&} zePhtmt*>9G$aP z8j~$-7ILV^jC+*_C?*6EV+Em8 z_S6H8Y_h3c9nFR!0SC&>>VdHy^w6SG@3~b&|3tLrFl1L51Oz2}vs&^>T!1e!0AE(5 znPQfpFzYY_!l!#<0Pb%9?jG%r0Dtg~9!JM$whQpqF);vdyhQ{2^9FCk1o#hFS~$GV zBzv=J1AwW`6@dH6O_bb*JthHQRHah51G7!dy%`-iS)%{~5q~rR#(bwvgV}_(@6-A^ z1iva~^C+Z3qzl^#WQ{Z?1Ic3v26FpLaam}DVWC4g0LT@?LSMeEX?owD7#2#9 zQEXP~OOFG{LNiS5Pomi_3;k;}+SST4d4BfInuT77)+7sc6__`xS(3e3t$C4qQYuD= zEY&G=6EjzP0d92-#Q}hxtNH$D1I9PI4bbjzQvI?W*sL<6-Es=Ae@l<@FgM#sA&47n z4K}N-qdeeyqx#014F7-Ojhv5iT&` z9psKjL~81!<+{HS(f&w;mZo-oG}|S@VbO6A;0|US$3$xq;Fpa|ZesxM4}eL8hN!=q zsNcqU6-E8?3p7ku?`nwpb(Om0?aw`=5%m|F5B}w5H$eT!8)8sjI8LMfX16+51+bF= zc~~@?Bl*-co+sIx)$K31&R$iKb6#|9CsmQaOVRX`R(igb9%!X6v(m3x>4jFCt*!JC zRyytFXkR_7bgq>iYNf|m={v3TBUbuVFO5qDUvb;7j$C4;i>&laD_v=Q`;C{birhWV zZKo>ogq42XO0Tlg*I8}0dCmQ{I>(u1t@c~-x# zS>LX((sx<)AF6d53$n6TR7iowX?!YS4DEYzN#Wat@J1>J=sb>Xr*7U z(o3!MaUP#nN2Yl5qB^qc1s4xhkqWE+i_b^%tGs;piCbg2$Vwk=rN6g$;1TcJs>n-L z+J@~@D}S4n-fN|IJ%@u6*M6?;OD^uIM+~qtdDl==IrFfcQj`se*vvUZR9?&k6>q%PaUoq^NulQ5IG^- zf{G*cOl%lp4yz{eWD|bGnna*erfT;K>^tG(P6araO~>MoKE{Gg4lMzXTj zpJeFa;4F)79&|d!WYYc!w*fTT)_A%_=Yg;B;1Wz^7sHeMI=VAoiI;?NK^h!3(%`;8 z2aW>OkHdjF|MO9D8p|Ei>(2qF(9v)#V9rAeAuGJ@abktzp%X6TI~5$k|?6=f}jG z>fjdRd>77|Hh|G+|AE=*rg1kcTD-?=p}%rd8YyRL*BPLl-&tq zprDZLiD$Ovj*v7anms3sTc`;;))c}6-LO=;5PYcQYh-ye$BiTyu5tgse2O8vrf=!w zL+b(Z6`(qgN;aLRPk`1FHL$~I6xEscerQf|v`0#Q=r)c4RHwZQ9i)b;@DZMl7HRqKM`{dVUGB>wfE zaj#gX4pTDYIOg+kxs^9&^dclBRmxz|Bvm{bzx4p9GS0Sh8d5u1iEJ0@7S{~5q^1#p zUc3YZ3eIkVK=(=I7y?a3W%B3_j5iQcJ=bb#sp7rxE0d13e`ZKHk)mHtjR{lX&y1(_U&&og6N{_MX5Y7hoN!fvs z2(q#Kr-o6>z;y#;)Aobue2{hgvzlu=Ufk+BGA)CHZPJw@gz65p!muqTB*!^ zp_i9!Yk3TaC;nSQd<`^OoEr_)&_ZYlxQqA1fV%{3YjELrsP4t(6xwrFUF2XM=WA6_ z=5f}0H|TZ`ly-eV7P9Sw-`xn;efN#gKj@bK{4(P1q*J=Xt^YVdSXN6hnANFT>e*P_ z(kL}t-eicGVp{Fs$4_0m<5 zDlc6fS#71KBdzyYc-tV#51~H*#W~M#oSP9k1b;J&Ip{Cd7Me7<;8ltTH*AAy=Q2tv z>=N+!?C5)W>;z*lR(hvYORz2je(Vf+LfsLUAvxo7U3$@f7b>LjQ6GpW+u^6jWn~0r zV}O2oM#DJvdk7btz)_{dS#ilK{Q_;+R^71<(-1D(vDn2LESt6BX&ILaK=0xhrZqE$ z=bR?Sko!Z!G2~*Aj|2a|K`9&l5nCI;AL#@BnFY8;8o1^|=OkPQ0+X@0&Gm5G7I67P zh+=G&k@p^ak}$3=2Y%lfe=Yzoao>&S!<|xofHQG_s2yB-$APO1>cQNY-GQ=wz6qnX z{*E->K|fSFK;$Ns`GG+BIN(|18qR45^m3B^*ot1Vt9-#Am@z+m8lDLTEvJBBIO>l8 z60Sb4$!y}Gqu0@GPcmdOMen(na=mP{=cYQKMufQs0d#U(w>6(36ejCo_1OuSTWNxUthrC=nRy>DSaGb*TRdOqiTGmWK1*DR zepR!D(Mw+8^Y4glXpJVK^v20*nwADmR_#=67k>xJIenCIB_vkVV0A=6?rMb z?JwfYPHmUKj+1`M0JmSSIs3rZHD^DKg+js#F*Dwoqw^eY1)`X$Q`!9!62Jfpd!{2R9tvad=mJ|cinquK*GGrYEnPms)c(pqLrYYTxcw#L`yYFA1HUrdFEO@+TOH)|KE zZ8Ka^QXM&bZd4p}_tI67Q;>G!^()|G3hPnSD5&4Lrzn6>xDLz?EiM;C`~V!@luWoD z+X)oEi(=YbrXwU7KWAU1xo84@(nOGsTXpY|Dq=b*WH#O};{Hwi9j_J@no$+7=wy03 z>P*qvT1+R4Q9wCj7vSRL>UA&yVfVD9T-N}!^wpzO@@ilMhZ;*+!^xW0ju*GosLkLU z{9JXv26H`r8mmsVBrVWZ?$z2RbV;+%*HDc$edbyw9}Ufg9yIWgOVRqLhN8EI;ur(P zWg3dFfEdb@!BHsAZV1I$aiG{1ba8{el67AiiVsamA>oUG;!Tz~S%2X{Fyi8~uLT!} z?Si8^pje5Y1{d#2(!#|eXy~LwLtGTbgzm=qgi;eQqT;s`IX8LN(V58$galVG)^I;l_A`Qh!4{2On6NTcE zhEQA_2a0`n#-qp&rlc`N)=G)T&J)$BJYnZ6@6lj={kg}^A4$?;=XE>dvGe%2{at2C z8cwUcSU8GQDQ*Y>kzNqiu2C06;TY5qj)8IEsGS;*2R<<+4dHlK^0~6pzSoNbY@mqZ zPF<~pqDF-elkEzCnF%nXm!uS9v!d<8GqZ&sh1d zS?NhuopY?QTxF#%wdyGA+q13wMOOX^R{lmS{}`*yY%4v`O4mLT#qBsNf2x)4jdT*7>=Um1hEfw?eK`D=k7?VLDAj>(Q$@P|VtN zkrob1m=~f%FlP*l<=s4-A#$@^$jcq9qoS(>DBH>m!-{9-F9lEn`x#OVQH;1X2iVBu zCHz+sg7eTDrdo|z@Xv18`B`4)vHfnlLkoc-e;@ZW$Ls8UDeIrxsI#pfOJ~zfXP@F{ z%t$|})3UFi!60vMrnBKvHoky^nWozx4lhWqLAN$+|CTuIAJ$C!`))_nK!2#w#Th-& zC{+9&D#X3CleP9<_EV=0l74VcmwPI7py{V62i=?wUb%gFaZ2CU9Vc%`y^u1tWe3p4s$`&M}CsMBWnX*pxDAx#naUF+e zA$Z;|no*h>M!xB$A%c79E@;hV7bUHvi#f6+*Qr4byBOF^7m*1~)9@3?r|x@km$$lO*8vFrtzl6R%ALzqnbJXBy99Azy?;^x75GS;%!18)Vt^s{IZ3A zz$_8GrftQ`qr&VNC_xJsOu>1PkLN`vfcEaXdelQvdEDJfUuLCyTIoqvI&7sMv(nF5 z=})cnYgYPiEB%?3?)-2Ro;_B6o|WIlN{_M9{jBt(R(iaZe(b@<=S#RVDs}iw_s%Ho zrof|xTX$oPS%eu9v!qb=>vxlL#ZVH`Io0_Zg@d7Y+O%A z!(J}=@nKmT%ni9=TnhsF%*U5p#|i<>iXkMbJivYHKUmY~dIE#_SXc?)Uu#$k-UU$f zYg9;{&7(okeU4b00eY5@ZAMi_%fmRpwbGin2BXy;>ZF|r{Lw@twcayEWijs0*tHJe7YKTHjR(4$6_EK zm&&9DF8}r;{HhcUsAL%d{RzwnR^!<>4^j*w7P!&Hdj2)myw68TY0j`?M2e=jT)^FM}KQ9W|{9gT_}v z2@p3;5ceHUgf!wd5N9rw>I4npz8~8`SkRdtW$~@?u=O-c@Id2!-SUdn zA~9P?N4wPm{1l1lvh|ztDjnGxBaX2w4U;YmV>f}0-cr7x6k`PssIPHr5&B&>)zDu$ zvalPFj^Z~Lxh39+K9-X46WL3L{XFK$I#Z^6o%#_^WW><$2YG-lelKNldl}6VX94}V z$Q_4gk{KS-w;#c|K@Su0F%a+GBx(XH$DkQRo85C)`vftWLv+9_s@lkB=k|tETxQQ- z_{waz^)ODiEU%n;1Mdq6kB8LiU*H#$bqid6dhdu?xTbzYtJh#Ko^dJC`QcV8@=6zJ zA#pTPqF0x6!l#gHD=4B)m+htQgkA9JJKasTx^qqico7mR>)~mnP>q^6nIo~q6q9 zL$Qv1N}VtLZWKtT&cY+=feQB#^}u2r7x14C5w3V;w^hLR`riDbH|IO&;D*0?yvEDB ze};tch&dlmkKpn2zUz>akl*cN|MHpx{E1(A=Sk)r-onQ3rw_x9=-QR|56`8``{_x! z2qDvNsP8FRjf+uMNEh;k`d0IXy6%T%=*zstTVvkh#oN47*AK~FUUC|?2}!tKxijx1 zgj|pKfr1|U@mc;FlD$rn7&E$GmoAj>U?1;ZEY>=y92 z+HR_)ThO9G0Ue;F^S_A!Z$!S}C#JwWQ5|^?zuog8){{4(=b1{u9Y}b&518EYI+LqW z=L=48tMlyEt3JZbt+8{5xVhq@?RI#EoBNAh7Z>ZxAY#BM{{y)Z+^^&HR1SBk`p8b4 z`M7@o($-GlEwf0VWCG3U^v~#Om)vm1( zarHwU>E?=l$fIv@b}$;^ftAn@Pasb@SrUvu7^DV&HF6NR zpX(35K}nV$o>hrI0fy^CW-H9IM-dSnNc^_`cBFM0=y^_GA|P9UY^WI;rg@Pz`kh_ip_g>yJOw8AsZ4GD^|2I%}EMwXg-^ zf=U?w-VBa$wN9L6#p+T|-bnH69bQA@kc(Nw#G$p;NvJU-d_yj@6&@PG3tRh!2C^T@ zsKoE$Q+sZdb*b++>U@|n6H(8t10RsY0E9p5ne_!OAh&)7CFkH6C1+tTYN!~9lez|U z<*;Bo@`iQ8(niC|Xm(huM!UnpdX#3L#l3=(=1I04tDj4$HDBnJpZz81WwcqL zmfCxo2m9yWh{6~H`^)b&f_>C?&5`7wxUlaS#Zh4$bW(f$>0$}%u=Z3{dD0WyA5h+J z3d?r`65RE0C3YzpQ7~ZK;)^wE>n4IB`xJA;sVN?kZd}dCwJ&*llN(V?46^y~8;mzr ztxsf2LcYI?UBLeF$Tx3EBNSXOeVaZSab;_Pz^Ixr_x#s!97(M1Nz$&0(a0N-@CCGF7-`ld7iE}k zhLQGEkY1XR)}G}u66i@r+9-u$WpNp4_Qs|eDOb9Q%ScC~7D}9B#`gMf ztH-GnXCgTy+;Kx7+%Gc_zO^^*urEHD5*d1vwLkRs)1N@Q;I?o_$yVM4&U^XtsU%k= z8W6HK+<7{KPczX0U zb%@kxN{`-o9ZA+ek1jd4DIK~L@B-j3$q28)8rT?_5!kMH`$yz1oT>BgiL^pJ4SKj% zsMpJ*3;2F3J>N)ZFN{B~CUb5?#AD?i&xPqXS=WaW>x z@-Ihz%=?SbAIerId%=s-wr>C-@i;--)-&*E9*-~Rbzey=!WSmjcWB#fcXx@kIjzAL ze8S@340YnmkUzMp7jaNm-%KKPnX$67gi^tB<0SQ|?kvw3&chdot|>+tCmLJV?;OrW zfe-H-E|LoU<(;LJ=nlTrGhdCTakki%LM;}TzstG4t!$;VPxz}S!IC>tgl-> z-Z|)oa2fuu$k)vXi!oyWhm+K6IlrmHO%}7!YMQ{#t50F)dc*vH+0=wxz(_R=S`Kb_ zW|>?GMI#>nac84i3&EgEO_P~-`~h-VceNCDLHC7+gp5W&t0N_9)z7zvOk>H+uuDelM4mj7Rvsr zWEdujbuy$)Sot_BSsfvC#jPJ*}7*h4w3 zi165Eu;q4l-Foz9O^S)KNw7&Vk~!YG1^bWG;IzYJG5%>)t_Y>}lBi|1|K@1OD@_q1go_xPkRP!$LmRj=t zie)kK{UMaObM}8rzPAKAAT9FIUK8>?huK=belBp@6*u||-$Y(Gb*12t^8HQ8h|2fJb%n50XjZe+_Tmb#gyK=wVD`uK-4r^}?=m`?Fo2GvLfT9vBBEtoc9XDKnFW^PP1<)PC1d zk|EAb=|lQV?TYtyu&Uu#E`IraLB2POU+@H-w}}^+D)9pKCSCx$-(S@z zP!54yZoB~Y>);DF-V2aoiQ!<48~aQ!JY?s&H$rxzCNRO+09v_=P!i)De>E~(V|g0t z>#EAnDI=>E*t$pscoKn?Y$EfDC~Z$*1&B{J&xSk62eumt77@`nN(lfpj-A7>_ne&3 zE1?)KYynzVM!zr`UiM!ZttU;j?MP!pBQF`g1q+c&!dfDi^dExz>^tc=1Hn~aYyz-n zDxV4aBi@oQu?70><;fZO6+3XDAM9j99z(aY9>Tz!lR`VYc(Dbq;!uYp90-WqR~=3s zr}KIuF9v<-ti&TQH0YB`5?gSfj0X3|Td@Uya#END2en7zF=w?^^6FFo^$}Z;4+Hqg zw3=?2EQ>)94TD$A_;Te^++Wp|KsCp=2xe1^FQV|*#Y3HrArL(Dl6uA8y4+R%j8p`2 zn~N9Nfd0Mc7sjsm%BzV;1_7CP0Ue{DJ_pc%L&pm|-S9IzUO*yg*p?eFAgyHroM=SM zbko}5$gN!|6?`!jt~V7-yudIi8!UZ83DkV5&ucY1KzY{Y|o zwY%oRSn-gqK3Ls^ct~p^n&`Er#KRew`$jJG6S=@RBFcr1A{Q7TLb-6dEf+3hQFaq@ zp#Tiz%7rvv@CYh z{0C6p{D+95Oz4Tc7>uQ}k~{yUlAixEAa38Hu|QbQGjV&t2!7xU;DzXcI8LwAg{99BG==vu_>lc6SNrZX$K==jR z8z+8fP0YmZLwUga5@do-kC~t=F$l_oBIHKykNJ#K--;j7@&Ikc$b;T7t=(o?I|jK% z9(0PSaIvY7$O?ENB%_@On(p*-kH+X*cTJg1n8 zze{0HC;ej-hegPPWWa70LPVcusA&*NG-gt%;KjIX1UoYI*Knadrqy%Tg6xM&wI#X_ z*K|CW-*BD$@B&npXd)N@a+l)xB|(5CFM;;y{9mRzlx-kh8uN#ox%zo*_w*((>+^2h zjn$|Hl83h)5lgTJ^^E`g9AxLc%Y_?HsrZefs01^;Y25j-GlP+Y{fJ|a^?*kWcn zDn&2CFLlR|EcMI?v7s_gvdneMRe^3Nle?{Sy&ue#(SIZ*=vf91)zyq0YN$HqQW^8w zEA^N^dyZRNaadY|q#hIAlhs!$%Ls}5e@xU3{NZ?#604M4E#1Wj^lSsD*ig*2uuuR! z4ke{IC-3h$IzRfi2W1tKO+vZXMEEob<#6dMJ}z_OLV0X6P)>k2!t|Vg=}w||jY8Qa9+X3y zgYumr%~SkQDQ^mwQ>3J6Tz+y9Tcx_dbqe8IMeVstX6?L8gMABBMl-`kBIzO5ANo4V z1CxKJI>gn#iTPEZ-#px#rQCSS#|VOIQ_`4+UzU=lM^t}d%!rEAuitTn&-$}akLW4H zw3`tW7FiR1&<@X7I~gI?sHJ?~ z6inp_?@V<)zjU(hI93zl(7CW4I$2%ulWSFL{KPUf{Aw{If$zhMQ!?VkwM2(TAFkGudnmE1oGOO%Ll{E)Y)E`2d^~{cJ#upZ~f_4=WQth20SU z7e@QPuSQ545m%%(@jf0r zDsPmCd!O_KLK%`x3xvm_zWj|_ok$v5u(;eNz)_h96O?kYVpA$ zw9g)@Nw#E;H=_ASHa()%CU~HTT{l#U221bpDRE-l5v9Z((XK(nLeo4qm`jwCb@fqt z0DnIrJh#XM$8N_@!*g4a#9?nkhTB`K<#l5`mP)U2)}|3s7=M`k&Bu)i*UbzjP0MTZ zmNOORA~i+iKXwrk>0GDA!FjH!J;6_wBqb3Vs>sy?o0%)~q)(B0l2c`+yI zZgsb`4&7WJgoHJV3L)GmQrg|Xv-V}E?e56$lpti>U9?j5lSZ(?(Ze*mAaA$xptRF) z$*56n_?awATLF#gA=M0;8|`+!8_>)!mrJ33rZx7Ukb5=D4yaxLMcnP*AETq&zg9O! z)0bN5F;@B*D?QFi&$ZGMtn@r9z0gX3Xr(t<=?-I}Fl1QiudO;gto(jf`XVbm%o^7e zEB{_A{fw1fWTn?y=`B`zrJKd{nUt#qB0PPi!w zXGbf2qLm(mw0+x==O60YEC0cLM-rbp_!3CRY1=&`1%o+pK*{)2Xihk%Xv#W43j)Ow zOcXR4Y<-xL6?=Ol6?PjvjeB@hC{UG@-^Ti(Cq4BfmfD=}2INSiw0UgEr4$&SPL<;y0I7pqoz%96VbHEo(P7+@xGLaS zhk~p}-s5=E{B7wakMhUy608Yl#VU9Y?@M2==o6 z!8*$`_@Qxozp=UAe>s~&jobT2Qrur+Ut@az=^#LuS3zWv8$#(s12y%$I^ z-z=?D#q5wa4IKWTkAOeo9wgAHfAqn4-H&Uo`yFRBJNQpfEU~hwZz7l(K(IrQI^7?J zp8?JpWjk~P`o^2R>Cgm*;B8ee=BtB=BNEe{=a%TvwHS?;=1kcx2AfM^=5|-Sdw{k|Dsu5KQfTq$0lV_xgj(+Jl92=g$Z3BrYTpKvyb zl46wf8JEc;eTf32mNlH7A4;JK<;8u%h#Ekj*93H*-`#JGGp5g-TRG0SeU>!W=P)Ux z=@-MAgf7ry?*dKcvH0n-8vI3F5mPSR#Ti4gv=DC)WXCG zj|PS?Mf&p(|H+f0xonS8{oykai~Ki^9n=#*zSae+BIn-VuEAB2%dGUxR=U(m zKWwE(S?MdSbb*x)TIt?ax`&m%(@J-=(#cl(4lDhTmA+|Y6#i$d{7bC-?pFFOD?QIj z&#}^Xp`C{J;W8fORYV9k@{eP@G8v%>?ZRjvP%&H;glj`O|Cgz5b2y1?Rf7i^EHkRc z#aL?isuiVmbbsNh8rZS${!B#s8C~OeS(#$_gp>7O+#*DEje!soYNl)(^t5TLYmDtp zj$rbNuU`82O{`6xdHliZ@wu z*6ilwV{+&2GuWv+li~fK&mrcrah882}addKr;-==qd>TK>K=(_Oz} zx~=}Ec?KBRT(@0W=mrq;O#(Ij+r~>6XT^RB?&$oi7>N1#vn|YF`e-;A-tWZ`dw0sC zl|}f%T~~_H1X{%OR8LDod^-}0X(q9LpXn}1CRWxn6bF-}mJSuh+~T9Y(U9aj&yQ?G zUGXF3K50a&);PqTv5?;*e%T-HxProhLm43l9lD(Xe1LFGAR2X?mtF01?w7A7`E_{n z9=PzfkoAg3sTV~UHtOe|Y}D=fY2;k|iQXUN;;kj*13w-sAKTcy964?mrGCLK z{XY6j#Vg^dl@8G45FC{DR_=$gNu|aT`Cxtqit~gSY`6*Csy(*e-^d2^&8ck39E`-M zE|sRFu?g;ZDQTd(h!1%KwME?nZgSOu4+^8p>0eg*lxw2-FIf3cSn2z$biI{sZ+$z) z%3o}yPq)(NSm{1i`g|*WEYhyNMc5@?bTJs;ezFSw>#jYZ7b5DkIbavJ&dt~YU}*Ff z;Y_u@Ja(0?TM9`pESk<*1?qJy11@Vs@+eU1Rd?AGV2kR;jLxXUM}cl|_g1TZlE{tGhDESFJJthd(?D@IFkZ4S)8-gtxdS!>e9#AiIf9I21@zE<*a4}eSXP^GVX?JKnQ|q0z5eoH9Z&~$~(TcL~SD|zdc1m$;U(D zqjU)*nntM`&)IQe6rV0+sZVn~jD>7_S$xR0Y-kR$egc^u^8(Q>*fN}u3}qg0TXr{| z@>m9zgL-v$vkga1=8!vQl;*toEffrh%iQ9tAS>?aBZwUkp3A62^}E}C6O=#R?tp8E zFE~q_Fsv=XUD6FK2l6ZnBP7CaFyNBw)t8lAuk_fq%1X(xNzkRxbKzZ4tjX0;7T3-E ztU0V5EWL8Q23MDog=C+%;?3%qzlHLeVBR~p0K(MVvFf2Gwhh}p2cUAL0Z@j3pzit%$&Pl&; z>bh$?oPgWQiV$Cu`Mn2+tu0F5h%hmnYP8jU(|~g)H7$ul$3k-@ad_)L>O|%n2_RwN z-$y0D{!cjcCI?q0-NgrIgp|7rA;)zAwg&FH)m9IV=OAawjv=aM)Tpk^!N`r8(FcQ3 z9iI`Zk`A7oqeH|fw}9Vd7SWhz4}tN12!>MBF9~Nem>jNN?x_H z-(1M-I`x=*Y!nD{bYCdEf9Hv0Ib-`Uss(r)v*LOwX{bPqYHat=FwpU`US�n#TUH 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

gV_IY)iLH;^an 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�n#TUH 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