From c3661e8cd8fcf69b0f560af65859810208308a13 Mon Sep 17 00:00:00 2001 From: "shantanu.mirajgave" Date: Fri, 5 Jun 2026 18:00:32 +0530 Subject: [PATCH 1/4] 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?< 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

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

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

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

SL?%tKlLs6wK%4S>`| z0J^jHZw_+x{yYF6qa4*OqUA->;_U^j3I=ne$$EdcG@{eMVTnZ%r{$-RmX;SHahTn* z)RpY|rS6trfk}_CCOB~lmn@-rr(xEV8t}A+brCF(u7-C=!^GVteh4-oztHtzNiD%5 zT8nHC0U{tr5CuC1x==8bfY@JGaO;nok@3Zk(7d+8ZG07^dt8snm$DuCF)nR)N><##~hTdLVg74Ig z{jpht89SZr;{N;qLryjLxqdL@ZZ9yG%bFZMCA(VtqrwK~Bac1%$a(Z;MyQH1OPya*?zRiT+d zvk;p!Hj^2MY$yy4xEzPlF@2tMaY0E_ckZ9m(ZG-qAG`xvvJkY!a3mUlM=FBC|FZML zimm_$f$pR#%o8Kmf-3-b6pl`s{*JoO14iHu%k|j$a3r9P#xZgbcUGZ4IJ3v;_9X6) zZbQfD+L%W_*?cN;Mj$%@S5Vja9D_*Y;U4kfY;FzS^AEnhjfPy5IrjqQuiM!UTU3sw z+Wl+NUbh0=YYJu86EZlph9#HMl_QEr|c*f%QT3ugD&1 zcG^T5i|r3T>ptIEUCYLOa6Slirq8gt^K{~Ylt{}0a|buEdV~+46G9XY&9aM$c0`av z?;sW3^5guGkp~r{1AY?g0r|SZJf+b9q6-Z(Gm*e%6Z>sImgoso0wH_xA^$iQMG4UW zty6=bM3`TU{c2MY{u%O{)LHE|%h{DSBu>3Kl^&c-*CoQqGIfa>q*MLp-nMi&_ored zh216s738fQ4dPNo{YdE;$BnA^Q*N6FL&1BMw_$K0205dC@F^J$gE{BGY6kn|*Bor% zfC+vwgGck3gjnaCDNnrx?P`B%y2`#v`y9E75*k&Y87IAJE&4`K(252DT!>DNj0sVT zS?pz{o+HS08V2XfLzi~S#8)<$x9t7@0TT+^yfjeXjHlTX;X2Mf@8xIM_lcf_WXk`E zPRzmh37wINXa&2(IZKTK(ZfyZa0K7`W+%>wBS~MY`KaqtWTCDRYsaDoL90&!&uYV| zA2yTFU}-G~r=GpOymuzgP8agBpZK8JXF=?PQRSVN@_x=;hS}#{et9maqG>^r{^EUZ zW0Ux7D~1oWw@}~rISvh=Zo28_BX$w$>vx)V(+!|L9{rkqE@6FNVU9{nFFkM@6K~su zgijN%yNJr2{}%FH_Gd|YN-81DX%O<;Yr5LeH#uN$!&v@5q^n72x&=LJe=_r>(%}{6 z2GAN##b@u%Cubs=3G;R-$D;+gd}WD7LHPDrV4{YFJN-g*q|BLrCYH?j9h}fODU{PO z%uH4KheVo-5Uk7K)Y-!`=TYnmR&m=3<<;7ylsFEkS1?L6Rl+uzelsEMH-q02@y@Kk zriMN?oiZouD|-&|8xieLDU-a1vEFO2v1}-dWducR>x?hT{?Z1q!A5?Ns~2 z`$xafoRq;K{W2mQ&;j_|8nq>YpZl;QG#0xt8V zvjMxIT2|D;4XULJRGXdUvX9RG#xv(zRt4Bt)vEr?ju-!S4LX}+143S{vHncKPNs?V z5abEE+%6^=p-1|K?y-CTzbFYO6pUjx!LMj~aDYqvwKb$)xUWa0lcK7 zI6R`gSP%A+@i{(K$je9GTXxCsnj3d z#=OYtgZM5ZJend0=ZQ{?&;ghTGNB-K6bWVP zwy??;No0ouYR`DgsCf7BPm7ceZIt?W403E!GHWT3$K1ITD&jZ0Xgfmje(jOIZlyiwWIMU~tqs*tD`bj=t!o zLH?wGsq8!8(l0d(jZ#q0JE1gJGaUN1Jn{-Wb7l$}ZBjgeb;OHj0ZMfPS84R)2TAc& zjM0CK6fXpXElKeTvh)_Dc!8a_NPyLzX*|!&QK#E)YWT(`$>SxDB#Cp+DqYTF=lMa9 zp6Csbl+-tUusZRu$3Cw~`oqmfU5Qh8W*~B7(!aba@$^?N`3(8=tAi$iQ|QwAwTt`5HgJ~3N9ic zb5>hef&%6ando~|CthLT%xka%8AU--SM)TAQoLzI#XQW29fW9*xEc_0zkvQ!1HR`p z+%hufDU%J z1~_TMiU6WR38Q607yp^V(bS)xA=S~}8nchVH6A>P#Kyj2_F?tH(dSXvji@;D?XJeU zbAMvF?Ty3q&zj(3{_|VBr+?N2v<$(v-{y4k32+2YY$u z^Z!_rRz*z2nM;hW?1RNV=a9IeV2&DofjHK{`(Mt+HWd;7A z@1Q4oIS2)Fo3bW(;%ZhSY{kqjV7PT{2l=Gb&gQTjVLOK6$Z=w=qb8n7|DAIkWA5dc z7io@r5Aw?BPI^H7?PxKGgRzPomgRkJDlUGSGyup%wzx*B0A5{EK_^b0$cAbcRbYP$ znCZ8SekKmb_<|Wl2@!B$!!6hmTw$Ul0w^PVTpbNCT*I*U5sldmzi(?8x_jnutG|kB zFAT#&kLtpsK*eniLkw<-;AOU}b!CD!2bxb3sRj)Ok7S(mP$7dmRvt%$F<&MYlu1sA zbbn$Yz9c{PEW-(`4rVUJnqE_P9xtT4;~J21h32m>GF^(a!$*6~PLcHhSGot%t^Aov z5w9lu2n0B>50lN8#Mi|MoI&vu!wwVIq<^tK4Fd6l5{d-vNR&7UzmpIm3Haby@eQ^x z2t?7Zxv8(lNBq3TnlC#{E%MiVcS-0Z{M$F|^GBO`p>l2JS`2wcz*igUiT!Pn`Dx_6 zcO!|?nlF1y+r-!Xn(+?tQi07Jd=mm6%jvWYn^a4mI)Qk)iEEc9R)}Ma)&VUbJ{H`H z$V)Z8zCo+V6Rm|<4sAMIoarGCVe)c{ke}I?BJ5JHT>-O1=m6zjgn0lCp=Q1PXpf7z ztS;2fEab;C5yUGH4?{Hv4uzMPKk5v5jrm3-8fbdjVo(v5`$gy^!%r;6Z&#+FMU_0T zL=kAK$YMF$>7;?Ws>lqXGQm-&DtnmLOrVV*0u{&sYo6SMB0DpP1mF&cRQ0Y}@(<0K zD@BKihyi+D=A1y%n4dUrw~6n+ZgtYev_Kq-&&0_4H)UOe&7eUf8JzXjUx9HBi@>Fjc%CiXp zJB4i3*M1P%s@wW0sg`2BI}4@UF5YLa^l599@4Dnxl&@`S?-Ni zVZq2)gU<{of$eo5Xio4VU5Ia--WI8etP%iJ zaZ5xgk=x1|{5m_2pbxrSxOO4~#{+zUy<81{@hoB`zOD}9>!*-Ot#B3MsIUM^4q%~8 zlC;SktfQLmg1$|j=rgPq^LQ~b0=xm_3-rn`+ZomrENk#VK~t<1YC)))@qU-QceeY_ z=$H1aVy(~ydF$Itowa<#I2yj^J<+eFp=Sh?fbUyRbgWbaWi^GPDm3Km#qi9AhS?3V zmLN_!q@!qvec_)pqW4pVZ%e2LRpOY1WC3@8(NhVI%4N!FXoDz)NYgAZRSL*EnC39_ zUT#o?_Ay){;yH&j=0l^4Xbz=77O0$>V9I)?sxQyTPPD@|f{emTrbYTv8^N(i_(fzo zC5e+*fIf%aJ3pZ5Cc95oMxqX4d}+gwYJNh*tjXiIcV5f!@BZ5@^hX#%@xdXNM2+lY zu6?GU1_hJ5fzFs)LEq=pw%b+`;LHQ`eIS28-9htZ3L^S^fN058BaR~fu}fP6B$6hP zlV<=;=+`iNDb$!soGf#__>Kn{X>pI5dl4+Rj$ofGjc||`i2{Rd6Qr8}%&#yWo|p%9 zB!=Q2kpqmly~Xz7-W4%f12jUGfPJ8bT^|dFG%w(=sk#{i`Z6jW^GAyK~c5qPQ*OS z0zN#;G$JT4B$1nVoSnuSQmlR>Jwq3?s|8y84Xg1Vof05=UY`;mPDR?0uP9(ZJ~p?c z1x-Y>M99a{;u!56P#y2`X~7mfxUEpwA;?4{a*54M>Ub;w4vZr{ zY*~gDk?q5pIv5AH;ME0Ku4TTH5rwg856%TH`CBNz)(8=`v9rUv&uPrr88nwjD=Ht{ zSUzL1_>zKJeo6(o-tseg{63i8bI2C)73YKS0&)!BN1o^%=ngq@zF|G1^_kpIT__9* zhXUY)h@;QXISD{DnqdWPh9z}^>B>vj#1Mw%FmRAF<+PZzWX}Z58o3W3V73of=L0>X zg#8i`0rMQ)$Z~#w*i^OBvR*GJ)R4mX=i-VwJd=eX=CoaemvCajzTtYw(ITZw?&to; z7TuC@L+z-HWkg3;_}OLMPf-fq%M?U48^aWZ+=M%4^jzV2G@F9!j*N=IjKz|1ge?1H zkbWZ$8!|QjuXM%+v0}fnOa1Su6k5Pkg)GQQsVq4U8;Jz}vG?sknsEDUtVzA#A17-5 zLG`Abz>sT<>z>B($mal13qCgnGgZsS0s40xX4gw(QrbO6*E=7NusTqAc9rs)>sm|B zgJ>BJoE*azwV* z)7NOO6%TijD5A~@@1BTegewxpd&sM9KO)5`)`wc~Cvpm$(ICpVi$!41+OKy1Ihnbk z!wg)2juTXg{uS;6`{i7*P3Bh2wRd=qSdZ8a_zwR=3|_f=suxz}u}{Ox6O@B-{E(r` z@QdT#=YO3AcfqVf!w-9wjmR_`YgJBUXS;!!;X5Ml=Z1cTSYd_S`2OQrblq1Mp3R4c z_&_(uNUc}mSL$d2jk&340{^Vz!o5ok?MI)3;`27>#~_wQS=pg?QJ!DxXab{9rs>bB z3iRP@2v@$+R!JN>cwxp>ftt^*bv2Y7V*Inak~cyh}p+AQ@gjt#8p>9OYUVUU#( zR3tO9ckJS^ZQcJ-%=w$H&B4Ci3RmO6e5+Ma1=p76tr4Nvws2i!Ms`+sHMRLz|EJ(q z1nLX24g*dg)IgZ;K5h&b{{5=&5NbUUCkykks8mE913{e`A94WX98Yu{XAIO2{s6=$ zSS2*8qw_iMT$(6woc72okM4)7CdiZc|FA}Wv9(cOOLC)ZNSvYb#CoA%X=G+LTu!Il+`qW~F6h`FvcIWEKI%$R)L}4P zb0~_@UNuEk7iIDGi6L17J53uqu*0NFjikCi_syIce$D(48>S3V~D}c9*gb&~hJS<$_y5h$nHcG`kQE}6QWJZu}zf+GN zqdPxOI#`Yav%%QQEvQc*HjiNT#gzoaUnqe-L$Io!(gNf<){IDTYW;nf;|QwPtX6bQ z(QVi3QgnNiopwT>eS>MfkJB8Jd8YQ#F65l$;!Djdg!VYGL%QNB0c?`O;=LYzA`>{H z5|Kxmx#~!;dJJ9Gz(Ygl4?N5hqYpO8!Xv2;y6ekwFqRhqlbCM~ z5&X&NFWS$&uu}V7tSxp<$4tfnBE^x@t6xdJWJ0>nHmOQOc2CJ42HQ zF;;#(ZKy?F^EYUbWh#|kOk$)uInnQ4M4OG5Hv`90N2|d(aCqiLRl=p>#e?yQ(QX11 zc?T$4sK~h_iYd7j=3dlmPfIuugR-md;T&+YpbjdEv7~-49lmg9!sTc>>(mpT)J!P2 zCM6FC%x22p+Xas@31;tJL3~-)fyg0QPzuG^eC9&0Z5Lk~fH7I`^@v}V9pue5R|&_Q z0k~i6_oe}ray~B9IyO;;Y=I9dDC+C=7rr+XLbNj-crMXOE)<(_3%XO-4`+c8v#e-91+HgbJ- zJ5QA6$F`jx%%N@oC8bCRC9=cs2d#ia*ar^+?E$$Irl9r8S8svuL~FyfO(XSulh~pX zk!R(XI1d52V%AL~56(JC7ShA-!#HAcwJYF4+JL#VPrdf5z7H*S!{+An zk1k4Wek;rbhD3&fi|=_5)i4@okoqA%(>9fp_$_t$m+4Io>tHzlv;^_+Y&5nwPg zF<%yy4PEpWDuwP_qR;Sc->2j4`I3T?zitIEvSE9cweHPMf@F!VP zTr)?O6VI}9GPSJJlT?6H`6LO18HLY#kfF{=%3idS;IL#a$3$pfrsc)?l<=kpYuPT)pQo+vyLx z1qUpa237#|;fW9&Zp=C~bgWBM;CT*s=|}fqLQw|!^iEPAD-C9Z`X*&XPqd1XVM=zf zzx&MjZl(Ut3&=v%6w?`hHH5ecS20>|=nB@fIM)-Mh-w^RD~!8fQ$RQ1eqTq7lEqGd z?&kn~eCx(nJB<%S|O*kA6uLqdR5jfNW^(+D%=iHK|9jXJcH*knojJiwuIBwNv%nnaI3u$|%q#^AEDFT@lT6~QS(#zA3 ztb|_H-2nj*#~K>{IJ=lXkKDr(twUZ4dFOsExiM`Q;q(RoR;0(2)fvqehPpjWa@$Evhn{Rs;^w*_!A z1;^It$BtgQazMo}cN3X-P%pHnjFfg?6{ay_cE}o!3kgd{;JzB3Lcb!rve~<jh|g@O=b$w#^IeraYkN>v zX-%mLT2eXM=JlHUXZB|$epFJ@MkTZB=b_uG3=0VAshagqr=Fpq^m;bW?2mPYRgFef zh|AX|JOdX=J)sml!=|U#vu)<-QcqM{8Cp{+zo)AAzLKh@CqW{ut^%+%o3}*omPq z;!AhqXD#q>tP}hrXfBw5JSS8~DgsDD%n8-ua;A6rx;Ml!P!Ax?Ncp+bv>gPaGj|}x zKV&+QLQ)>@$gE11-#WWVZo@9S*S+K+oRlniFCI%SIwA`Nwm#K4BQkJ4q9kth%$*FL zZ)MMi?E2Lc7`t(}-ZiuF%;udUW3K!TZgx7Fcf9zGTPW&sSE(*J280+duDwHY)L2j` z&DrC{Q%wl)7s#W;EzVWlj-~1g_9XZuK|d2`-%J@+ZpB-9H2mps=h6+S`kQ-6{qyYl zGu#!{p(0jzD1J3)xWexkRsq23^@r2!4;zjoU^XOwv_IUY%UrobZU<9`7Z4^;ZMt>K z2r~1?hN@P2gE%hTv1a&1Vz?OA;ZtaXAOa=U`y!(^EWvgK%x(zl((MqUzC>MzXe3Sd z+Wn?E5?HpYg306YdbQ-cvk%2&Z+a-j?^=C#6dF$4Bp8L>{WDJGc(;UbGm5&n~hE zw4u^Hz183u$<{rn`8oN(pAmf%SNGWpuH(?aD|frsbVnC^W-n4>1v#{+8b?+~m} zM0H#_+;1BVLJp{-G4(X^#V{X3r%kj%mvLi`2W_5(v4MFO_69}lEZ&shY>=|3Y4-wZAPrS!*X@dAxD;{`I>BDiFWc!3vWLw+a- z;TG|sc~*J0`8>RSBG3DAj74?o;%rZBIVQ>Ctk7XKFK5;duFSOGK=vmE)V!Pp{W|i@ z!Na+0!D^6Ge^*=7P;_)whQ(SdvdqSs7qekPU=AIVN56FtBMFy5yG&;+QT zm7O5;e0DTHP^r56C^*f;Pk0d4fYd;iTG`Ra!H%BjKk=zU-#zsQ233SSW>8n+qf}{Z zg~kIaK^FAWheC>9oEPc`tyCemkcHS~6d2aodB(|=MYD(I)v)FjFTJS$pa*Jac%E!D zhYg+zVJY`Y~3J@3A}o2hjoMBTP3p4*Rr|?!vvGhk|3K&kfT>r#c@8WNo->?HIQ_IHR97E$Glqb}@o*Q}8@05i< zX(Ok-aDlT6FjO@(;3XEP(v7;;XeYoai~Y^#=7t<=mxkpJW7wIMId~}!4*TCNb3x>LotKy4G%r&Ah|CQ=e(#p);y3X!Gcw|-J;l%TCx5yjd-&Jy zM>%f)X+thu5Xpa?{CUBB%ACJN82{C2w-y8TfwjP}7YpACBk+HE&KGP*#}*{hXAR=z zvs&!PhjgD2;BPukoYdw=Ky^@XbApGJ{AJK$O~v>E7guw1y5P-ZdKiJ)#~9lxjE@_s zz?vLJT|CHNQLDh3+-h3se+Ibt8@zL0Y<@ly) z4|>y^0b`ujI=0O$&CH;h(@S!d>%&PUrrD&LXYg*$*s2H|8#E)qu|9xf+E zwIQ)=LqW3dBy)e(a3(SwBj3m)i$EB)OWRrep}c^%>lV0b5g1dYxahdKLXZ6g3b z&KtJQfZmSr0bVA{(~t7;xv#iE^>}wmdQIi<4X0y?!|wUOI&vXi3q5-f0xNj5(QZ#P z?!rCZ9$Cxh_28c^2lx+yK-OleO-<2d_Yyar-7=pUzx`o%H2&;Y=!ckqeh^PTp!xUM zY2u<0aA0mKXC=e8Inea`{8CK6g_Ap8zl?P}0Ly8e{(m?wdX*Gxpg7BSur%f9Q?3~s ztHp1IUMDl+Mm)a#le@Ff8j=UJD#sU3dk#`A5h>SsZK8XgBaAadCBcIb|BmsF{A+b; zWkuUn3zP20WmowON&y zZSQ*WV(qV3p(}+9J=$A92JMy7Udv%QRs$^_k;(8aJKr2V_GT^z${bcX{;*hD7zmSM z4MHYmTml9)4d}xMrf^{y(h?e8);CKWtyIgaNjI`G1btwtHH6E1FbyO<s`2+adD)t6C*ARQrNd~D|a z-3__o6k`TuCRZAB%wWuR_~AoUnyImYKlO?7LvQRb`f0=l3J3Tqe)l#e)O`QK z8`NLRFvstXPnTRkfM^)r_=zF^6~mJpYxKWk1aAF(zh{57M~3{a^WXEkSs&RHk2HSQ zl$+vL^Skz=vJ+i`-B|+E_#G6*`*y=?PlZ2>gg>kT^zes5)A`-KH5{iRjt)-d#MUIelkyi;ugvsBODgVMr3l*&Q!vfv!shS&0m8=g5v zYkb9&7Hsi_;QTe2dR#V(7!z`toJSUC#svD`(@=UH?p1(bedZ`K>rmd`peHGZI%Soz zcn ze_5-Q*STj6z8ZtD+G2Up;W2CSW=nCVjnC+5Ej!d!;vg_>Z&^PFaj}r&qkY*d=x#KQ z`p-;$RUe9+wUxzuvz@4;^3O@8UgtATP!-R##H@`O7>xd=#&;Up*?n=KcRKq_9m`~~ zN-d^O4Imug|LI*eI@5SZQ%=h386P_f?nAI8Tp8OnUhJQb0!@4!ZOTwx7sa)~t6Wsi zpIG~i41gLs-fdf1WG0~4t|xyGPq|`@UEAb=!6hWSJ=b$@nB~{W${Q5_i|Yk$J^YJ5 z(3;k3?y=djuQ5^Jkr_x*=N^@(a!ct_J%3Ow!a)+2lICeIgF$ud+Rn^gRy3eTcLx1X z7321|#rn7xTQWbgA~pE?q`pX2F4YiPE^)BRtOY~-VW5`edJp-nuoqkCQ(+RW<}a)6 z<|wCHpA7cdI$wUKfHLV(6FI_)<2kLOLSMjMx&C>95Ml*OKc^Ey1WX_x}XF6;q4eYTUr9)L(Jf z-$g-xt0-did0WMXRV3U}uKySCS~#`FzO-AW=Ep{o@jXbc*Hi*x&b?Cw(!nJqSwPgU zO4jG(m)?J&|3P8@pIzbRyE^P&mJNBhpp@t={?MTFBgsyBx7a-aUBzo3LPoV$SE4oC zKj?U^8lmIDu;a3TPw|c{N|Levjl$pdpjWG)S=jW=^l!c1vx}SZoobL?T{(JPK!!KA zL@2Axri?vtxd==g`f{*uh@%jD!>;xZ2)bBDF#y%f_L2E$2XTru-h6D*yS_wUb!TIQ z;+mD99uGrv&Edbea~wHxv^Q*_bVC0LPQQ?p(j7aIhP4%Emz2zWQ9fi9r8tenln@}SN%E7lL`_^RNjtIUP6KZp8hR-fxC9bl)O}lZqtTxoL+n4W_ z`k%eMA>yyri+^tK0(slgPGFoTBW=&Kp=X6OZ9Q&2vme_(L3n2GUhx(lJ;64;H7-MD z+wyXMDeJ~%^u6-p6>FOdWA|27&EZ$VyVVM(I~Gt!*p8d-isoO-D}mhz=R}F6hy(rm zVLXLOM7}xd6?T7Tp8r6zz=o<5UhRMU9Rw6++>cNjaHxt`(q62+irQujPra7(pxuHJ z22yXh9;a99wo1r?*|lj0zvX?YVq4-vz#f#Y{;^q4QRseonQ%{DqY=$EiO!K^y2vhx8Ut`Kg|+Wv#EX@eMXf|befXs5wBgiGZ1`i=3W{3c{w=-FT|}THg_L# z*!q>S9`AwM1#Caw<|V5_|5D_Jh^Mgm84X1`U+YDq^O|w4HjnfBe(6pb58aPLz#cI^ zU~O9th|QV}9Pd9+(AUc!oBd^=YPf7JK7?DGFhsJn*^y5Jnxq^E{C2>)`pc^s@g&c%ESQZlmx#13vK;@T}1%gJ*Lr3QuqH#9rXM zV#>~W*ok(}3T%Cy_9LJOa$6J}P;E3%VynKfTNxlw_`%iRmykej2!SRsTtj|&E z?*DcUj)M`1UkAtYqy+vi!f{8owmtE3uQ$IEj$gbH!f|2`IEqcwTksyw2$;vcewx8< z;!Q7(r)v=%r{S&*a-Ygq|2{Q>NW5Kt^#rMG81d9A-Cgs!5Lo&lROc3PW_8Y#r zJ;;48HtVlcyT43ylO8^%#TD*mp+hz+N|slFLV=xSU3?8S*CAg2dxX4}aIHN7_SX9C z0qj2P3jPy(%G^rI**Q=RV;$dTEZpSH3?nM;BK>*Rb z)FYkm^lMw+xAilQ?gzmVb&}1y=MsAIB9FEImOAprhGjRS?O_rk)$gDSbZjK0e7s)}&X=gi@>Aoo2S zqk`PLSo|xk`Ic$qbGdp6n6`umBiZK?&+D zIGc*TUB%$Wx49&4eN}WGXn4r%1*&IX-n1`0ML8}=PX^gcExbB%A}H)HB0m#1@i{j~t z6gj(uz|3!k@zi9MEv~EBn1~(of(-H){5JWY zz*Vs(?q4@}qnJr6*F~rAX~`+8 zN!4K}7+Y+X;m!U?Tr$9yIoI$>zWq_U=@Lx6D8=vAVF{gztAHLL%vqwSS^Pz)il1trP$Rj`wS zQbKbTVrM@pcEf`hJKsr3ef)qzD!e*uRRzjvZiRB!DR*AnvpU1CRvjH4mih^9`pQ$! zQROu|La^>_);l;I_!2eQG@bwN%ObWYN%4(aQHo&M!nYuz*zmDGl#}E(|MgEr?|XcY zp3z9f9KL6LZS=p*V(56Z*RG)Ak=X3rp&Pr`y3xNA8KZ45P*zBrP*rfz{+?xaxjb!% zbDIG-oltqu#yIMf_;}yQEGwZchJPs(tqFr z8OU$h&;2u>r{DC5g$E#fzXhB_d3*Q|a&q;kNcy-)`s7IZCz14&Ncz4ba_w9g$zKx5 zS6^9q%e_}P$K`gSFywMs<|8szkn!wEr3tZfE-x1wT=r=}+`BK2a%4RnA0l%uxmySj zgUUX;OuvTA=0cfeQsyQ8>zok#*%SQt2>84zHh4fuyz;(6iZp+RgNqW*y;c^*cAfA+ zJY@~rie$xR%QrDQl?M0O*2gdD+QKmWp)7p&^ZqxV`?7s2p1yA)MRY!tOyAz1r0(~o zKIg!|7p8f?(U>FC~Ua8AwnkeNO@IM1U#0Rgo*;cHGwS7b_!-LRr zk@SvyC(e&1K47Rs-$aZeZb zyd&3|KK8og2XHC&UwhV(ZLuztrw8nySQ~A3^w;Mta(O_1M*X^#tJDu;ne7BX&&4a2 zwRG2cw_mNfr~IL7>b%S6kxp#>q%OVh(}~pTI&b$nxjnt3_3#c%rk-)n1y(%S{Jjah zA2F=N&WN7ibKEJ4HOjA0`fXKlT5u_4?tAul4?BGak0d^$I}|zWo~~<@a}j_ zuYOlE5;uls&JmdjW#Xxwv|n2b#wR=@uteMK3vJ0jrXup(5{PEuOU|SX`3l-YDB!Sm zdF;{);897aLCcu*?l#I#-@S^K@HEoNYTWb02P{BAa@@o(u%yKYucCfILRkN2fMTw{ zb*WDhxu3N?65hjWY1e;a z`POZ9sT;L`;VPtV^!XJmMxtw$%IP0TEt&ph)P&l@ab@UxnK9Y@t6uADKT$x9U^+p0 zQNWAlCHpab4g|Ykgn!t7Hq`&!T z_#lBd@aK87u&(0WmZw=|$VjW~1XWfv#yWmOqwFXp)*?H~96frPrI^akWB299?mMr~ z;FYoaM)euIx?<(zf$@}uYJ|GGMw#kNxVN?~jZ^B|wp2|yyKPJHlylm)44Tr^wxwvw z`E6SYr!TbBe>J?sG-#5 z3pCQjq#a-S$c$EP+6Ses6rEjvPPCUUs zOXcj5>(t#7)yGxz0jf{LVC2sbe&jY!%-_!66m7(!WWa*ay@*y9(L`Y0Lp!x&5(ud2Il94q3bJ^>3qvRF{9s zlR2$*=y!B3l-(b?^TkAJvkR(%hp5xL5CG|$8yKl6n@GxNv^<-#f4~y7nS9>kT|N0* z7`fV+F|Qh>2V9?wJzOktpUD3k9J=LQiRg9mw~44k&&e*sfzSK?OWd*$0EWlg%JZ7P zCkM`GaD-UL2`rnY9if)o_?Hf%Q-9CL1gR^V7_am6@l5jy;>$`q0gUV~ADBuBf>N?1 zZLOMI3RkWdrNyM4BhJJx1Es7kHKB-!_~E-*2dSvu&FW81GF6uFxPH;5-q5}7Hfbe! z9h@UXNI{ ztj2LqYn`b0EO>M3U@%7RZ7dur8sMOncR+sSoh4nH)v z1B;XX5`8FGj*HjNi^*K2x5!aLH$l)F1DB=&wyBp`P@Mq2e`p>f~=S# zU27YSIdNEMJk5qTh&66&U^WElqNQvz%D&-0u!yiRj|FtL2lPHHg*pvvq)tl%5`Cem z=u{bV)wW>25KnO-{}F%sM~=7}^kNySQ6KjR!ptqVQ)2h-px)lp$hY|eOECb%m;m45q9tWj#E3@|Gd<(bUS*m*7^ez9WH&}j9_9d`8V#NfLg zfsVFWbZzmy7gnG^Z$GfV z-+Zr(G9N;3c6#@=kKZx~9n`hp^1FgNrY5!NX`}LqO7^WKmJ@x^SkShBUup#WU%BThvwZGG{VkfAVsn(1^QsVO_qhNy~QybL{ZR zhYql(;72@N%BNF1LpHi1v){ovHaabmJ}Hv^c_e*dB>j^}`h${OJ6lOdG_t^-xQ;+u z=1c5CPMUI-Og)i(RSiG59t^na>IMR%>mIW{8Y9MiCES9msJq&MVgL35+u z{RP<9gY*^yd1~>n+6;M61P`mdkPxbE^e+~yAiZ@e6_B1GRzP~(9W>H&dCqWZWpO4F z=~;f3^w!c!Kzg6`AibY+Nl%1k#f?3kzk-g*xx+e+p3*e%h5ejRPk@N9_UZ0)YY@3L zsShIDJ*&?d>y4*g0w?{|pWxh*TFLQB$3xU`{2Uz?e(3o5OavM*kZbw%f7c;fm%r~F zgdM=p+Th|XA%i&OV+kaWfYJgRY;b@C>zt?G!Igb1GkO`--)Wb;uTLx-F?kJ z@pCZ&js>9e{y&m+{Z+w#^hd0h7d)x%xw8W0zk$$KkDb<|4BEldaKXBG!MYgu0+wiH zZC%{{Q4XW}PJ7~uh6q@<(|asAmj0uham9z?HomwGXR~6Y{#|vE6=R{-X&}Zq7K_xv zm9w?H(Q=}uek*u+kX;`~*f+Y$7YCIMa#+vVJ?}25GLHPrj-*2U>f8pqfE(};t!tx^ zyJGRNxQG9XU<7V)pMgVH~FzN7HDcXng*X4DQ3fc+!!} z7dTc*`qJXlK{7pjTht)?=KTV$^+H71T0u#98l1lh}J<&km1}7SuJr0LwL@H4F~1MZ3y_Q+Fl4) zvr$4b^b3EAv49BgzNxIu=gpIZa5^}%!9BlKl^6f`R#2mf_Y#=3l>xd_ew}c4qzmL)YkoK&} z0_olNz67Kf6JG_S*OqPIC6b;LNl%WXe-LSBL?j)Lr0XN;#z^{{NP1c%Jv)-VERy~f{m#={ zShlz;|!C*y10j^zM2Bu|Ebu}d`LXiJumm%bgVsZN4EOj!vL&p zInn6;(Xu{~l6B=&jwn42vtHXaZw}aE20o13p}9$3C~?z(z#82kYV>dXXFtTusXSr| zRx~=7l8Uvl_H_s_vKf{P!Vq=6HY6XU2ksLe_mZ}p4f>6O#-RYallNnT35DSBj)t^j zi^&64RyNunr1N--`G}tv);iH+F`p>GFOhcvnuLUZYSkzPIzXHGg#* zKwdC*$q0GV?-1--wNhBh8dz{k}%{m#bgldW*IHonx>2)h~6=>m&B_wtp^VHNIB$0GF4a za+A^lkN<QeuLcNbh2Z^N#4e}H*2kb1Rf?=Iw% zlMIHYn;zxXmjg9U_JJoieXXG{O}&+Noiq1-0NFWnWBrsf_YQO1a&jc7l3Eh;gk zh=VjM)h9OVBsCeV{QY2rpnhtI7Jb@3(Rrskcqb1!s4D_y$LD|SKK{$ zadnWRiRfB^yFch(Lbjb6!g>l(vCCqY-=@7Dd8KegaWHbLVjYdj<#(glS`{~iF!aed z!Vm1tkIF8@gu47P>xV0K^b^@6{7`19OCA;H=6ZHylVIl|I1>5aHdV&apFJR-+wb-o z;LZ#shzZfos$rj#|L1c89{!(PEJiT5DqpI$5@HBKxuI+PKi&^uV2Or-GVrv!5@a<} zA~<8Oyh~W=kWEEYIl4 ziIAes3U661R*6fbNT$T)57nkYd*-Q~^4d}sVIR?A+M8MLbM4{s?X zw32G_?}zarP9!+qbOR(K9#=#oA&+W##997#5M;o3!zwUd9ohcY7L>bp)r*U1Uv@$c zuI@3)@PBq6m2#cI$V4!)b{&X@jO?hOgBz`(TT0f^3eu?gD20~t=V5Fi!FfoF9&>5l zhqo-ZarXede#5;RxI(i3lkPP-MlE{=DE5jb_GW{g$)f#v|}RXUzX2Gm;C^lG~%J5wLDpY&k824?C&L z%w@DDgf8Pp$ecrj3h@obmH9d4viho1ePSJNY=V-$;D5n>M@S|)8)vib&zjaR4_Ysi zLXwY2zx)ycoMhHZ@8umQO~t+AFG*;q%# zJN}2QB%VI0goZczog81XCAIN%|2A0H@Z+EP*nOw}50L9i_OO$r^5XR3j_!ER!nC(i zjTtP^TNbGevB4;s%&Lf7vfuYP8KAd|QA!9#{Jy{Y5&C&#Nj1KhkW)YZK&>&XEOPod zfYBERGGMuX0z(OX;VOZ7)<3N$d}n?Mw!S^?5lmEomda&t>*}v$4t56UV!+!!1nsD= z1TBxQg`lkwA?DyZaZ?1YH}!(+Xg(6epdBohJhSuA{|KIaL_VN~gD2-KMgCH*+4q3y z-ywsqh3UU;&B63vyKNU=XR;vK+j@qprqKNv!1;f-F*pnoV3Df=tcy3^)n7j&z{S13 z6IMQ?p&SVJJ@e%cKltE-5Y9(`6`W%o&s!@F*6h!0jT*kxLd(Nhy!QVIh=aIHxPCxD z7Zho9=oP4vA2e5Xf z`?aCH@cVtqg0JHl-(ol+&p5p3{~5pEN0x*fGQ^+=emg7*8Sls}epfl{teE>o`G1Jt zjbCaZg5QN7Z6Ck;V2J{$D zgrj~*JFqAArt%01(Kj$2ybKZfo3`i_;^(F&iSA7^L)G5prF>@=>L@{{MHUsqUxj~2 zGf_M0IEs~U(SPGbnY*IOSghj$Rcd>*NPF7gz90P-`?KjT%4Pz5CsZ7R>8YcbQOdl% zzTA621F?1)$#R_C9?Z(>f00>XeX{}<%@18?}B((hgD=_B|#W=Jpm{28SAwe<4q>vQz7ZO15n=I%4{ zT83~0Q#GfLxUk5UTpH28+Dj%H;vJ3B-h|#!s?Y^NZ{QMIY?>A41 z-FsSI>*r@Qzn>nnG(ECd#t)Th{p_0@KE{q-8&%?zLIcWcnP7P85o^9Px%C?MY%Np`8)~9803>VNPJlRLipZxHaZ@ zm$TYn;n{p6zSK=4OXYVG&`0(vt);F^^3sM5{MoBt@aGNN+PUCLNkfwNx>HXjQcrg8Z1mwxLfw9^`NX6*9yi2y6zBjX(yTo; z*a|)MyfpjhjKx?NxHQWZYK(-F*u=fpa+{o@JoU)`isQtutBSQhz!Z|+I2gp^TxBgz zRQxUMX-{*)@n;iVQ+%;Oce>8os_$8kz9lHz)&P%OAu+I6V!DM8W zOssjpmrFt%DU3Zt2fx@%8w&mPYdH(f*-MACn==i@**#*jo&{d_?Py<+;->D;rLbLHH`~PPlBG!FWQv{`~?iVE_j^bh<3a$o?~C)tN9+!qwHsf`Kny?v_j3Hhj5=k3mQmKxfB7{iTbTZ z1Eb|}SND>fnY)5{aqB))Kw3EBDj;98V?@VbC{EZq*-X%yfKP9vdGYB8f1W67zu_l! zL>BfER3Mi#><7 zx92i>G!0964c8EzU_P~0ek?i7r=ve+eO*4Gn`uZfvCH4*dKf;W$vI`c@BnT7O!I0V zOR+@i{Y3uCSVuLr!*QT62uyi$jEn$BN`@w^f0>IY9|YdAg=IksGxgR;cG150{Z}tq zW`N2{oGgvg9v4yl(|*GOrkl!iD%D9|GTm5=my+NuNs~+wp8+RioUeAD!(mg`$SQKl z*86$7QTwf@5Q*GWOi9Yb+PnG4?x$s^cm+2$d3KwjFV*lxpH)`vRsTitErRq(C(X}1 zt*;8|YgX`o%@4fh6l)~F!@*xOrEa|@_2@Yc%uV+@Uo{7uKx-2;GgX|^Y>(L-R|;;- zg_Z)hrmKvZW?lkYvJOycEi=mHI9Ina|WtZIbpII}BW!TLF~ipI60hr2UjYy1R=#GNWih8IxG%e&FVr< z-m2(_JH*5nDR%o98Y;Jj##P~h-JM9+bWjBR;Erzd1jpaKN@JC0m(=sV1+J8X@rNKf z+uLfFq7D98hk#^l%ej(zDym;uqT-e2Cn9p26aR9zmkiHv&-#XA9_zsiVhVy8kQC0q z%xAIN+%f{cS%ky{kdD)I7rIa)xTv|*-{-gHWl)+=#Zp>7OwRz&hIVC{O-^-&ugU2k zTcqU`Z36_;tW;~+4~Zt&MOr&EZBntlzgr1 z-6aogGpP;f1B|-0I~K}cr{^B;^jw}QmY)01Q2Qh}%=`XY1g+_usn?T(xuWyp^gV2a zW+G8}%Wv6?`=xVEy(b~f(kZ!jCVAW#tz4xpg%H)ql%BYr1DIlL2vYDxR&>+ zOMRFa{9@t~h9*KgiS#MO z)hkB{_YFz37fGFgFWpofMwi*H->Av|)9MK8ZvKHdHd`6luEu3qHX9=goN$O(FeBjj zr>Y^p`=H$ZK7qZ?1rI#`zZh@|;kMELg_2soj1DbKPr<)~?sc1}ER)nS!1z>G@8iMy z{6}X5e6M@9KlO?nD%O1ZR*&=6s6S&o^)CaLg}rKfx7@#co}7${Js#}7aYW7Yy#;I2>Lf7-T&n}a zRxf5oYIU+^R`Id0@il)XGDHf^!}{x$5KZLKa;_RictdtS&ds)PsIj>xFyijtXulat zolZKL(!205!g$4LgPJdrW%S}J)g-YOyf^@yTFh<9M8#w56AgS!>rh#l7zNBaPIckw zApc+!WHbh(iZmlKTlPr~evGZ6pcK12EvN)^)?Z|H;f#cC?8e%wB`8vB#ZVIbX*;qc zFE+ok4xV19NL1^oXb8(VuF1#k*cCi9z?dt%eg&x#=wxCmNQ_7&fxbY{D zhYnXY`Z{gFXb22v!XPR5%Z)x}aViaK7EZ^=WaY=u&dlQ)FMoqeq3z5Oz|ti*2-kKm zcEX`6^Y|xFWcEIla(l@423CV-jhiG4VDc=r2%|hA1KLfabuS(NpP}y!eVH)GI8PAE zRZ(CSETlX>j%SEB5_6jFS91yF{UDyJpJu+*NM7D47C$Ofb#H1jFSJyN;UMg(4GYNb z2WYZEO;&xm$+i`7|2yAy?eSi5uDzxL9!>u0_CN`w()Qb%bcbt?_n33-#iw;VHe=*h zjc=CZw3@8nev{t}n&cg1YtpNq+OcK^`)L`nr7SD~;wbL@QZjsaRZZe`R#Ga00?5U+ z@CWTHrYn>HQ}lo`9{+``FS57+7T92m4a#8gs?k2L#zldhCmf)@t4hH}D@bCh64;Zo zc4P=+?oC){bM|B!IV_$oK;REpVb!dEW0qbO+@YqB(F`bI@1t(aqAFnMpDZM;{B<_8 zL5ciVlNBEa_Dg4^FSUM-o^u$?z1@{f-C{~lXrEt>&GS{+=Z93a{tNrO?67J-J$7kW z#mw=opY1)RZ`%rNTN0t4nRQ+RN>;ox`44fn(XIUGX`LT?xk%S_o0)1v=;%cV*l6*S zAID@98uet=RL^w;#z8I&+D^Q?lM>)BlElTkOIEyx;_+Odcx?6ip+2C}?3Wz;-eTIp zjJBeuQnO;lqLhzPb34&^vf=J-k+p6Rp$!mZ1VJc8#NnL=BMuwh=HG&-rs0E#4YR}= zCMrzDs#toUnCcQHHk1EQnpVvyWPMZrqKu4zgHTbr2cU=7c^43_Z&ULL3GZ|_`Wfg0 z-4U&6s~>lLF!=6Z@XLD+e)){WskIFLE?f+0kWhV_T5gYH0Up$R43S#ZF2{kr+TG@V zfXd3)_fHId#o9%UpI(*I^!Pbr2VynAM-Pps-CD_AB`yu$HVoir+`oxUu`r`mQzITa z{L{#%INrCq6n~hij9$kJ<)KZ!d$%lq=`sI=Hv>~>FBmg_H_rcj{+j%k+GYMoZ_U5w z+D#wcbKMG<|AEo@S4HPv<=>>evF2S{rMVZ&X1V}a$n*9Ng9$r*ch}dQ0y!Q6`JP&+ zTL3C!q`OcF{&B=9wsVkQk2)6F8VzxV6D+m|#%AqYfI;&hl}-;YJ$3OGNRS#6(D>my9mgN3HuKI~ZuKFdEm+@t_Wy{;&9T$Z5&_Bv323zUW z_gpu_1Q0)3e)w|4D;)5|H zAxQVl)P&2-#BMijv!g;-AmC7BFdq~4Jun}G9idc{qVx~W#bpz=w#-8PPD)txpo56r z=3E=(2$1+CWB6SeSPRJLMl_j>1d)vRv%85nvGB)poo+{?h zXgk^SRH(%@@qgFuiqx84q<>m_%Z2Bqb)tH@}kx5vb`6;$bx9`mw zsI-uo?`QQk@?BixGUe+zs%&|8fA0geGb7uMdR;ZgdTsfYB$%v=**$4XJyrr~aFLyW zYtQ&zW|1tqCd46za&(T`{dU&F-gvH>>-q-DzV{1CODL(Ew_D+iOC*?Gmzlm4m;{N0tw{5F9>&ThU zdt)$P%@}Hx6)#V0b#lX(D_ycu4f+rKbKADcvksf~yf>0?cR{B){hIm>)NlN9eRX;c zN?qkyWz${^*K2uBA16ReHdM+OS!lc&hefn-MBmU(^yYF6^yREPbK`AcylCKWhZ?vh z8H{(Ii<#$XhcVBVGSp-P(#yTa+roHz*VlOWW4slomCbz7s~ZM{4M;p|gZ+wrQCwRl zZm(XMzbBDsqyr;esTo2=6GIZ z$F{`_fPdfSBiQoMo*w3`aWR}%=?Drt$ZoKjsXfY8>urge%9i43&$_oOcZOC(5K*&E zjq-Y$b#m}=UGSQ?|1-C`FRiaJmHj})yvzn-eREPBPk1ZwDSIyvqej% zvd6OP%UCHw_WxH(%koKvJdQElSo-m2xh4{ubDce-YX4slSi;*%KHol>&s+H@kx$Vrlgmnf5QVan15OY(tro z5%X||&L`jk!#glZHnoPVU`6HtKV$_fGW|)1`dIZT#Ll{ZhcjbAK#G5vnHgh{E*2!b zANIibQi1~Ae5vjePAVi?BuDtfW>WA3uVA&F5@F2;b8VEjvr(1*7Hgj^NP7E-LlZ`6 zbg2dUQJsqCn_qQNl=q#X=6=8p#X^P6SrJ^w&z`Mk52VR?$DMi%NK zfWP9LtuX3y3ow6h`(Iut?o5$guWrZ|`6YkLcejALhX!~~{I^Oy4t^G3Vq!1_jHPRu zclI;Ck{TGk&%TyH$^-yUAK+q^#bn*dfquC^Xl4l4uFRWn<>2}b>FQGgU?x&q&BkkS z*DT=e1&H41$cl*bJ69Y0A78>%BkvpsP(|Rx5Nz-yT6Zsa{$YFYw4j|Ygz)^^G$Gm8 zJOOiM7MRNdU{*nR?I8w;|Zz~T{CAXO{#jP8DQ)td6=pv!l( z!u2a_i-;S|DlC0G1oH3@pRBsUd5qAJ3FiRaif9s`EA%B509w~G0ze-CprcXJ0{k9J zpxzS(WyVp!vI1;aow^c6k1fTvjs0Bk)z6H+EOG_#5_rF+F zaaGAe>X>;V!TF+wkW4`joEM_j-l6uoG!Al`SeJ=-&G0savKdvezibw$ka}L>PWB`- zf%l6>nS>+J#%aZg{HGKMjumPDt}1b&!n!Lfg^5Nf?l1$K1ZRyJ2=dysoT$8~q?`}I zoVXp@wh|kcE3N+Xx~szZ#IYb8YUfQh^H}>nh88_$AM5C^%!*fAcqeUoQC`a$&Nxtz zM1up3261wt)n0UH>y^~))sE|pbK-FgYsSb617K;Jmsq6SZGIuFUN*m?z~0-uATyZ? z!FtP12{;8Bf9_*4M%pSnc!x`E>zpBAAhlW<3c$g&kSYl_N%||$Y3(KRSlPtppnmGG zrxWS0=nfeT)cPIza^?n<%6UkzWp>+ovw!Cj4mHP-AFxu!9@=F zVZ51p){BR``#!Pz`nUP@6H@(S_jSheyE1dW-zU4@7Jc;ozIfQze~(vw{db?m57#%_ zemvLz)Zt(Mea;WI^SvQge__RTzDMS_mhh~-`k(Nfg?YIKHrJ_uty6Y=K57U+7ZCt@%i+* z3XrJ)n+-j9ydBta+LR*hHD^t!QRVUUsQRu^#WVu}j}pP_ZnbGTh3Sl63f9o7ix? zt!wJoG5$ZUE`V0>eiPe6=yF$f@AhCzr#FPSs<*E{1$L*6)w%kMvapx7bI&;XB)h?7 zw`9!3r-rb|(a;@Jtsztu*HAzIyDF*OBF%owFAka=6f}$Qd!>6tupgt{yr5l+Mw)#u zXTQ(Jx(FN_WS38-7d671w8X6LGeqcGC@`iTDkoP0GPS6T)X+_lHLqQzGun;@@?(QL zRK3f7NRo1(0D{H4I^@u|+;9NX3!t z)*xe#yLJ{jH=w@B)cDqTs-sG|vORIgClfdaeW#BH!i&k*KTJrqb5N-J#b({3nRwRD z{g`QG)tI!EkS|ARoNe#YL|RaZd-K&Or@hsu=m}xcI$`n*c>dE*z-+zgtqnhRMuqb( zw8dNCjzh=UuZILZt>(AceyqyvvyYCa$FKcK^{LgTRi9pc#%Y=1ly>;iqu4$@i}6gx zh(C6s9`6Y!^-pLCb&2Jf2VM!)xaFBEBk2bt=@jYk{w=ss+cSn6*rE7s3z3W9L>xBW z8zbc4^CV`wxOX%vSiD%$A8U6ZZx?T>Dng1uDoAIm8&5yfNE4jCfV7XWIBGE7wtlNW z7^&~f5;Rtt8m-8zHP7fUAC@IJcSLg?UCXgS9$3seRzF`Wv>_PxQe%a zDABs(Dgs5$8QIr0zB1eRTU-78I9jguj)~W#KGV<|+ScQkYy(P-EKoFrjjdRB2<5hKBed=+F=damu^_-FU?THTJ}x>r9nD3@MY`!;4;4>LBhx6x2?ZMjnhio`o`AJs$%W0<=QaP4BV(< zyN((A%a*3Y$j;F>L0+0bAcHXg&+ls;6HN7tHN9_y8VP z;r*qyBj^KsAj|uj?K%PQ8Z%z;~>+ycXRTi5~o3?2CS;0g?(Zj zNA{C?cUJMj*5;pzgcx>w+>9hQi{|{4=~P!vpBPJ@qKS>p$J|<5h61}3tz|V^rBcF_ zcMoynvCd{**`x9HAG*d%7q-Sa`jW;!tz1A^$RVrjXUTMn6qJ>pIj^gFv&A`7)QgP{ z?502-td`ObwK6=%JYR!*@k1<+odj%9pMd*3{-_K8?#KDvp2`}ScQ z3U`XIl32%2cVUbU=n3s6wRaca;EqLcgvA_v8*`f{1{r|m%$+1TH)B62_B(FI^uVV& zUH~^VEgX3Hsh?f&NlLSJSWz^w_suIo6rFA`+q9PSw@ zCo{G=Hnvu+7iul%!kxjIC!ny*3Hamzk5LUWkHE|_-zCEdkoh)!?F1MEX3j$Zl?~z* z0Fl{)afkYw%Cl9Dr#`cnvMBrGZq>$!PHP8zfNOr5j2SD%MRq zJyr*WKE4y!ke)B>i>DVE+NG|uUnxUEfAD20(C_=-%29G`_8%cgN6oKdP|A@$Bvr^J zc%H!^z35$91<%*1@h)2>eLzX*bUBiGR3FV!)Kl>fz(L43mw&B1@= zT%GyMU!c*ahwoq*{X(k4=XWwSze+`ZJP*7{*Ur<)u4gz`k8>F4U;ujXIx-@9}NFiLd&Xc;8q2*oq3G2#PVM}97JeaPTSbq&zmdWEds~7==V9d!smNy8i5hI!T80Q^*2j$_} z0sc;il>cT}{_nlZOTl*SZz{~S{|*ze_OGz=>3%;dR>U%+=*%kef_>=xb()Jqv84cS z|18?Mg#hg|=>XS46v-iB4o$$!5D#-0LuQDPIpiTz3nSd@>Im_O%n(a+C`G1L)|Cvg zi_8#nyE;NdBeN)5QaGo7e)|(W@NWDhRxv&Ns2xP>;HD7H|A5zr9j0xS=1A869Mh5y z^~5ysBpMfg3Nh`)E!)Sm*U#AwrV$Bm3qxJOx6Fy8;%TFShb6?r1q<0h>Zy1tT}Ds; z|6MHqmKNid!o7#f(MJ?#Fg3rNpZ>4;l;aJ{`)R3o3CeMWn#;nqB0iK&GP_ouRgbU& zw-qqf5#b2J#xHY*YajTx>ou6?JGRFeh6<-VBN7-+z!_rgf8l)I--BvU4@Bm+oC zeAn)6ncs89CO#3UYndcvjBlj--MJUuarfr6V;{R%$`Q}p|e*z#hTQa5fEWupHSN#RVNcIss_%^0vN?fB3gD*Vb|QTU|A@w|Z8_ z#wWAgA7~|13cU-kEnpxwFl(m}Z@Z{4uX)GT^1N941r!AIj%g2{O6xtf3V(&^4z6x` zkDESn3g6P-<}?cSCSq>ESWP-F(90J&tp<(awzF&rTt7LDVl!uS{uO-7yhke7r`hYM z&82{KP=VoKD^r92IZ=I`{Yse<=}&2=fYZ?Gv|~a>GFk0h#3bTvZ*6V8xX*xCI}fx` z?quw|Jl#Ni{=dVKvDqM?{5QGkt)&YWfZ-!vs~`aFN+0|*?ib{@bK zHzRp<2v?SK&Km}fMedWaG-1iZe+@8N^K@H>SoTZAD(jm$TC)E~V4`gM`|toFli^<3?hy)^WI zsL{FyZlk2O36-2ehF$)~iih)k;N9&G`6ZTD^VPAb4Y_k>SpSqB^)KFj{oe)kuQzP& z_r@Nuz6hG_P8)uILce7$MptqV0bJg_6Ljs?HIPTmf6~>mV5x0AGkpwT4;hO{5B~{u z{NpH<>GvhF+YZq+??ds8-_!~l_&>(jP?edbA@sob?NqIxYEbJVt5r`e|BatP1=^P=KPq;Y=JRsj+QjFNZXf>V>6B^o)BwVwP8YR#^b$AV`TAM z=ktp6H}_)*vtsr19{@qsgT7@$$iqUG-*O^UoHGK(IwpwU_b>!5PjJp7fvURBv6qW> z>3%n0oiy}5^#dN6`g4wdu%~X7#X_Pj*yeGV92;EQ>Q~>b#cr7s_v+?#bxfr}v;fZX z{vK;z$soJ>+fNrKZ%$W7GX*qK`MJg870<-lJH=ztBYDYCFC-OkqL{K$*2FkI+R0e~ zG5M&j_O*eqkB3Fx4yHMO$OMVx z`D$K9;%m*>uXaIKRdMr~=bj^5F(zVtxl3O#6|bx+nH<}-ijAo2IaM3jiQRg57?OG1 zZHk#Lon7Wb*=0`A4N$HkqlEZ7Jei+|RbcvpAIB@rynt$OOrz^CMK~tfH=w;_0&F6$gLi<(XH#X;cyK)As?dgz*k(K={^u`54#fgIsrl z?jJI|0-gN#ZPCU;A?Nn{*)HJ6lH<=Af9?cM92~Zdy`!sY5NHI#81CrU0UB-Pt07Gt zHSeria!05lY&$QeM+%D#QSWCm;@@PpCL*O>r+_&Xs@D+2?HBZd}% zhunV%@M!+D0M9O5#lE7RAO6Nub4W9JW%hi`;fKza9_j?Qg7ETO;Q&WImoHNowI|J; z;htz>2TR(ruXG!C)~uCZ{83AGve4DBilmK^>o>_E3hM)IVX;%-06fwBEv%|x7_Y}i zPj{@MUh$m$HrBA<@cb;~mWY#Mbu?`?qT3N={us+|)`iQ9Id}g|TDX5Yx1_3daxZ@q zcq&iky_Uqt%YfvN@}#|pl{&dVNX%U};zy&-Gd8OJ`EZUqx{e93mBV__^-yD=wF_kW zpb4~NXPPLs1!%d$t`JFEpUBr_s%3#c!WMkKN(*OiliktyVwTOlOjf|&R7xB##8yoc zH0qeDd}LAk?b`Nv583cdMr`fsxR#H`n7caeASpOw-Bwe6li}~8g{oe;NMB>^?KBY$ zTxnlHHMJR7wSf^^Xm!dZq{3~BG#L>-BWY3+yk7aL@Y;Y7TDV7^?oiv^pB~Pyq#J2n zY8@@|+E~AYcf486y|LUXJ+Ihr{SB>o7dLXhF~5HzeLQD7@5}LuXH)!t{st>m>(+dp zxnXwN^XhmZp6>X7A*{6jgY%2l*?=5`QXTpQA`$$*!zU?N7~H0)s*nw?j7adi#xvHo zhpY^*wehujaS?^xiy~OicWzzJ>ESxa1*s#@&{ZB3#I|b4<^f!im}G5%dj39Fu_s;3 zH~r&C(hQ-_seJ^jFLY|JqCqkJTj1N+Xv_p*j(%pK#4b}z_0Z!_=O@`L`TPZ^m+>Hw z$wO8+l`d5sMf-4Y2;INE`I?zu?Ya`>JD=yt}B;iU0M08XL&lV!ePrj zo!dL?Ticf1VzVqzPWmA=bN}uFdwTdUUI&B^2@lc_H3efxrkr&@aITl%6XR*L@mS|p zFzlQ&M}0}9#VeU7f#PA%q4)mSLjMeFeG)=dw*w}%hVVPX+O6Za8m(hfr~`qzIbmhH z9nfw9z?kXqKcx}#BRwc$LZ@1x z9$Ig$amDEI_wt-Ri;Toy6q{UAp28<)rP8mF4tHajhjU8cXOVQ?(p>&uBKapr@`p##10(6pk@BxZ(#uKDn_%-$_o4nE zrxeo~E_rvYQN}}N&Tu*+RET?^Zzt;b=uu%eo8Qe&hI3 zU3#{Qvc#jvw~&4L%cKL8a;cb$38~Ngl*aN2gjU|JZ~q9d0(`aHcVvM|J9qVd-J7Xb^%%>bg=DeF}F&Z@!X*IjE`~x~*f) z%;6asYCxuT6{fVAcbt_JV@B0+>BF`p+Pd=nyZ(!p@8l4Q^ZJEBH>{z?P=jx<7|c^Z zMEYRTL+-D+@NQ?F`S3-v&JDX3LQAA$y_qvs)gB?!wCZ{V}9GvuNFo5Cby~N&;tMm)-@~mSuCfXJxu48_@x#-+n z6dVYhn*u@^hRC+RL!VJ+n)oB|`1^7#1&%mT*_Dfw9c%vvE%ngp^c9^BXaqVP@34Lv z3|Ttem_VHqETqnjVt>qM_4T9E-A<+XB;%tDzcz4K_!&7#&xn})dlhBT>{!EWy9u6@ z%}13+v%8Ud-PL%lgIa;k*pWvC-n^eXgt-~Lfp3X%FVv(S8o-xoR+tObs|%|uo{FV+ zpiE8bbFO91gAyv9h^6JPl0{MBX0=zlZ*|2ngJ!lr~56=}%i0yoWGzUAC z#H602lvC&FiVaLh5mWa+y)CR##drUudNF%D#iPo;Rwd5=tGyZf!bAtD%F1k&B31bd zRX7=2$0w@DC%~PqQ)N(qVw{r|tLA0dnkwy2vYOW|APv3^U6Trq+FxVg?oejnNf4X> zq&iemu;EWFb5918J=W00+`gbBZFP2+0oRoWYw}brHQ$=_Y})M5RqP8H!aSQ*S5?)y z#U+P>-NH-liBzDQ0xPR(zRU08@HgqQINM`2>N#hDRL4SonFFCDzh|?pXBJvT{nZlc zuhvh0wRrlgm8AV_R%w=B^sK$fG~#f$eOU>$viwi@f7xp}J%i@v@qOsa0DU3}jSGd^rta_uAlomd zqV99Ng5*?kgrQyLX@XPM56PSpWRg_V`c4~~hn*i8N3BD-I&b8^wpA>Tr8AP{j?t!G z)rX0EkV3~{kXrcZ4pQ z@2U)4Jy>N|57snudFFitygJ(W1kQrX^E1C-&9d@}_aZA#E^6sS&aq^?Hjk5=_bv7{vS>;mQ|&E&ps;@i4|{Y4Jlpkv13JeNB{sNS*j0C{{A0`pV}gN18B|@ zfm{)(a@m{ag-nXwDjmw~kz-Vj`dD(&$$yt?vC1^Be>tCrE_1F?9xJQq2b9QI8u&GY zvG{)ypYhBuNp$t+&$ILzUvg&C+2gUtP72B2kz{a$9?&EQPx zIM{?#ed)@oLq)ws;Db8<3W{~?OBDIvqu9RJ>R{e!n2u)kIINJzzxsvkQ{#fV-qc6{ z3P$A5=0oO6QqtEu#Bu>t8NY2Q;iWcglczzzLqAwBsi?O*sH==U7&NyPTRv{_}uEj5>prX&rmnx1fnD$(VDc&N(Z>K8c zkmY}ZAJyJW;(P>NWD%VIA0xYe*uqelnPl4eSQBQ#XC#xB-$^FaPIlyuRGp6*zZR&J z!_ZP7pQQ%NPdV|H>JqPTTz8$VXBLBB-x9&#Nk1EZesRgr?|vNye+hY)S_@g^{ohC0 z<9@q6G2^iKSnnjC?F?{X^W#7J?Dlc^9|CA^iPB}6fxfpq-;0-lVwVfG=he}mW706} zgjWum6hH8r6Jm|GP9%+!zh{|=)JiVzMcB3;QiW{~u{)HLVcTq-x>=dv4(0A#r8Cye zt+7wy+!}jT=kJal+Ke9Logg}@IEE;wPb1a%-NbJl77*!TukPXgPK;f<;J%szcP$RG z2x2pU0!CtQKlUnY<6G9ov7N1B%oI^HJQFm)p;6=C1nIc_&UcS7=qMLNn#v!e;D}7D ze5iaf4ynQMgzST?GS|x)LO%u>{Fb-K6Z-OB}8t!&?eNR^o^# zRP(ZeM)o!R`=79se51%=?`26vc$DbdFsE}X-4LHylogZh8 zVB_2{1W`EcxnTUykuIR*|E6%calYsjivU3bL#NRAIMW~knS*Ulhl`40;g>$WeOfIW zW!Px!%V~eH_J2SG{$6~@ta0QJ2UD_bN`R&N)nhr;ss+0=5s*I&OfvIDrbg%W*g0e^ zfu=LhzArU>#514lm6y5YL%xL`Wt;t(w_#cOSb1*Ont7VKe&*<$)#%DwbJn4|Bk6Y{ zX)lt#KazeUlHM3e7u=R>bDv)Mz0T+D5|gf}1ok3Ll>>T%iSzo~p1o)R$mHxr*bAL{ z1ItW$H>e(}NP@eNYr!{n0xNcBH&v=R8{?}^fwIPTEzg6{fW(+5?)8AM#Cj)anlh= zk7$I!odFv%4hjjcj?kk|fgdRN953Y6ajR^1TrQ5>ZNskv+9vyFqO-POGGeW$>tX8jhv#++X0X(zX)DC?w zHF9ElSZlznm%YVtBo?d#u9O99mI`xb=GJ6n8HTJGV}Z4nD#-5MC1?M{-di?0ebTM$ z47s#0V0FTqwLra%doE!SHrTq$Vlw`9_Ky{}sqLA63$FCAk=G)sVQ%*%FqB}&>SQ$j zZ>|vX&tRe>OEF=MWW;sYuy(Nh%E(gsdoPb$OW7_*mGhS_XWZ0?`Pcl`!8GM23`Napj%x#lh(yTyv{dblEw#t5*YDr9 zu8qq{cAdyf%)z>c=D^<5s<&ySMm7haO#WJ+Jg%N;RS~gc1XBv(SY>d`0+Bd-z2;Q( zj^=bu{Y=7%8$JYPzbp`^Omw|Xp1SO<0{``S>gtqUzbQ&{r7e;5!a&LrHSvv&5H-mF$uC79u~V1~b>+gk5oeWB<2S`cQm zW*PQ7vfkzy#%BnzRO{Y*!hcw${%=!LGx6Ho?wn`ep^8G*RGSKhrq?$v*D!9^SBBA0 z+wtCvY7OHX&Tg)Akz>;X{De1qu^Pz1LDnrzez)gnpu!r+&L0XD9m?v~uhUxAaY->* z{_?{y!;bR<8m>v*EPLwF>0$5j2i`NauqJh@SfPI`19SE}#?{Ee9Ovwsrl=u_)LLjz zok62HgKBKf`@G4($Qv=w9+xiJB3>)g>|VrQO{5A+>MAz2+#8HBw1?s-#mV`L41yCh zwpYV{hc%Rn{uQT7YTA5~-okYhfak^9pHUZV0vSb*n_FB*1CUuhom1%oBdLGj|0VBD zz^kgR{{I95Mn!L&W1X%VHO@gr6G2Thfg24PXIdv()MA~YCW=)cHVJZjy+EzBw2Gyz zeYJHcZAEcF8H9kfibEB(3Uy*%qoTF~j`@GSYoBw^NkFT8?eo6x|Ci^1yU#vn&ugz~ zuf5hJ(Yc2!bKu)OrhFmP_TZq91BChB|P^UDa!_#ZskFa^F$xIeRXaU#JjMgAig5PaM zyk@jmJz|1WnN|S_z6b*a>DGK&hsVC{r_{7PQdom;lk7htfin2Ke$+Ewr5Y$m$37h@ zO~)GRXTKis?zKjlbm5C>8=!~U(CL`&wdt4i(+Hl^1)2zdbOqKF#~PspYugb zTY3jY)~_wC3$>+m%1zCeDp5n1TSL=VXpwpx95|4Lb!+m5Xk((_P@0x)D}Y3->fFVlUUl3aa8&V{^zc&Iy+X=!@a z47D06SDZa~pi6fK+x+WM8pR~MNR7x$3UJ&>wt9Kdu482$m}~CyibC5}fpS0Gv7vMq zC=pi$O2j=V4Rf0!7=W3_;AJNHpV1=DxmI92HKkcIEuxe6J0k@RIF>$k!TMqKl&bZ> zY7Lq4Cr0_CUxktS2^227?(-=Jsf)&PUpW5STzGkv%w~KcUkn~s(0p|B!?m0NH4-;? zovO^(QGz=7m%DM`{X_A=zEmyuyMaKp58OjA1M#RsCt^Gm`;M84)9W?uV0s!PGUQ7F zDfh(kx`D3+Ck)x5xeeKa=4VdCQ}Ot3E{m2oW@B>EAg!b6SN*vhVjssErZd6E z0doe)KWT-isP<*Z}wuu*cc=6%&Z@E`(slubQfIK`-CQumy;pD66 zw}<0Qrj^+Pgi*ei<}L>~eZK$n?}&QKrb6xY!c%VYbB6W(%m;DglU7xpC2HYjDuUKatE{nFk7F>eOKzrb1@z)*HyV+ zcyJFed|8?*f5`h#*}w=4zfgG>V);K%_*?=K1vFt%^)CN`W1_!Z^Tpt2jCD^4{N|kX zL*Ta$)YyALU;~*R)89+o&kJ#!vlL_?YR7rW5_|s z(CO=BBL0We!5+1R=hH$irVdUX)B~7~Uk)&>s}A;v?TaBw>R{{v|5NJVpN`K1k=Ao~ zI^q(HV=-teZTPc8b-K0$Wm7-`}_j{^@Dl^MByHbjE|zK96r439k`rdT3kAPs#L$PT!uzhpvOCrs$>+Lw9j zWmgljLLuYz^L!B#7_ah0;&wF*Jl3nArBV4}hE;2QvA0+Wk4|gMbE-GJ2#QqXi>%-? zT@v8OZRAS|>g7(0;*KP9U<|=}#YE3uAHf2KgZQ8#zg2*9iFpV9EsuIbiPba9srxSd z_C?=4Cs}qtXmAsr(T9jPWkpjJ_mv%pAQMpkqsYW7*N}iU@2sv&HeU?aeYjcPe7!uu zu&eX?sh995$Om0k*pEVAoP;t=`WmqYbMIaMmRhVC?v0rLM5f;aA;?eOgj(4`-r%n` zdwF4PdR7T}u;q#DiEI-15FTS-4A#vWZ)iGAD6-f763pBi5HX-#H0 zTc?4o^HNP_09$8x(YU9>t&_0|zB>OPi_Wiq@BCtJyJh@ z>kPH6NW|C4Q*UtPzqNl_?_U4@nWzQoy?-tvlg$!``|GqFLOn~Hr%F{u_QsZG zvf4>pW8weY#rVz-zw2?}zhl>sAm434{W|yGSdD-2xxW)MeBrtO!MocC0tgsG9{>O` z$xUH$@SFmYXq0YLWj)XR#i{i=_lKx@{tbooAMWJ-w7DUvrV8|s<@~rGpAE#K3J<}? ztJI|x&%N%1#B@SySKM;nc~#V4-{YU>gm&kg-UXMyO`LPjPvMmIyX3p$c8gAC*G!2{ zQTzTVO79a(pBhV#ilr}#rH_fFn_}tLV)b7d%ik`Rel_;Z#j*T&-`Zo}T+TP^i5K}L z?w`s6q4!ZQ&Ygo>sA7lZs4{*F^|B4M7VE!-YTxS;-^=(qvQvb>ksK$srTi<`^;5id z^+!3#sYGikJ9dV&xpl2O7%0?nlaG6$!M#({=jqm-yVzjIUq5~IX%NW6!_R3Reev1H zHHDY@;6Qba83f@k@{K83RK5LWz*}p0sgEnD6=wha3QhICf?8xy>+Q1a3hJkq30%1I zZC6kq=5=R(`zW~PZM4yx(S6<()T{16=!jiGz0tM`*R}IEq0^RQm#h-^)KuchuC(zH-Ef|DPVs);!q$=eQVr;+^&47<|I> zZQm0w3c@SWx!_X1Wy1pW@N zZv)s{<<7|fduNg7Ufd~yz3%JQi@$or!_zsjV&Ro57G5%jvxuO^eWteY)%Z7BE@o0! zF2><|yJ6I^+(_lR10`)nIZI4shY>h{=xf8%kLq1Y?^21{`J4>a$=JKLtWjAc)6(*1 zYm&-qvd_pV4e6849WWWB#zfsx6j_g2+|NXNrFsAx$z2$Qf6J8DwZGVWR6}}RYE)fm z!=4|e=6%?QC}3At;5-3;Oh6iFCdp-}EdqH`79<1?Yd=k|MlH>3`IDNeEw61}iTE*G zcL){Tbu@QvF$WomnyrCgzC*gUo8nzaNEf2{8C_d-PU$%O&yxhml1FBWX3+Z~slm-< zO)XC=Y8Pk8Z@R6|m7fE8YXvjqDmH$J@P)j?kJKx+O;dSFm$&X>5^Pk8z{j~@XTzS6 z{d@{*axg%$KD`91z{>hP=hatzm`WV?sO}F|EpFK*_j8Iz_$4uUGS6J>D#Al8L4X%G zkFCvYURzZ%_H`mo;96pJ%N-3>w9|Yj);Q($nUj;4s^dYUK6}sxsj9Vj>TgIiEE-<5 zwq=X16VIJ8{-m(3hN`*EM`jMKsvO(Z{$irz`zCL5p_}Dx7$xt=V>lIlRAGmTi*thz zJ4`-rsG8q0p+1cj=H(^1aRS<^CtDtFf3an(0?j3n?G<->W#+O{SGcb@kkgXlp7zJC z7(2z&%Zgjdx=!FcAe%ywaP|CR9MGo-*5QgO`pWj0z@B{53_p4j@d-0~%XV=?1!Tg< z`qkWLySl8}ee;q&052z%E8UK2BHh_FM=Q{%1Bmvynl1u0rv{fKvVYXzNQ8mC|(MVVy!h1%?8h3O$v zYSaHnwJj{FAqZ({ebt54&HJL8phS@_p>5x?RG+DvB^;c%C+_(KLoJGvQIsAst!tk8 zne=m7^8k=rVE@JDJNIWZyVE!?X|iz&2&>&e{_rJtC{xaC4}(N2v}(6IAN-JNBOG-> z^8o#7d#~0U(9)Q^1L#D@d&;}!5ut}zY>qnYEA}LKNKN)g_WT8%a5RIR+c#W`ms*He zOpjq3C|AG=YcbmtbTDaCo_@JLdr?V4b|k9XRqlwrv>c0|R7ndnLSAjAplrS6&0 zg;FUdYx3*!?71+PyB6#zdTqW&9-6{{YEH4RBM`_STDaNyWyTlB@Me42 zVzvf>NB(eutVo8AFpS6X-{?xQX;?~4M9UQ+$!x#Tk8Z>CHz?2rcEagvH0SQb?@>B) z3QYIVk*V|#TGcwm3tNs+0KBEbV%b68dsnRdD!28J&${p1h)?WR@jDIKer2=0Rmx+x zNk)1wD9~QELIv&4^mlfU|@s|0*cSeP47(S6%s^qkQ zC@LlHk}^T++U`WhamA{SxdD!f-FY_x((K&hsYXCWb`u>Or|!!|!SL+G_+w}lbxQR4ygZ& z^-GVd)yrk}!1~!sC?`edlGY!c*0=i80G9fsrCpe&p3=IG*T(H1$g3=mTGot~GLlTu zG`?tZe>n^ehASO4Rlk_Q>+#EoqaoQdk{<_SMVz={zHsN%xyAD*67e_DelXPe;cC8f zcWlL?L6xa3NpK=;~RO+NO)b;GLCY5i$PB~R)mmxOYXfVHZ>-Wonpc7TtLsd1Dscf01gI-wR?EUBj ziHfRaiAg80i-R7z-N{d7335WE#&a)pC!Y2(yZ;&OmX1zP3kZwBDCUo=yaOlA+kwS& z&K+_=^_l$$8BeUz*P5%sYM&Tj_)(PWmR(e{)}0ge!2o@t{WaTVTc^@QOXVc}FjaWZ z$$PeQ1N~+7gF!TDhTyjLd5H{ijsup8p103;<8ytw1HYPV_P?(6ixq1>i{taK567qv z8Q3nNY{Ep_AaDp8XG&Qu@S z6V+s{s601u5xIs+g~fS%{Jhl@`RLrlcjn=j?x=Ft!HjT;OV-IF{H6Y`O#{1HL-5s5 z(HB^i6jsWq!WTj-WiI#wA4`etTt8)AjE}p>-g1glqP6Ps5+8wQ#D;jBL zB*O0CfiHF?0{I}=@+0B0UsOZh4q9;r!xsKNC*MNj_$O##bgTuu160dM_`$O^kmE+v z!f4qe)50dUPC@;nsUOkK4cH8$m@u(4mLqVWrS;m+zYJQ=r9+`I#;zL+Ol z_1kULPZC93;|};arP&5N*nWBC70R@Zt6)Rk3OQhYDi!@4dFpWRoNUTzS|j&oAlA*M z5yVb+WAJprn1dMVHG|Kpyn1O+)0HqaLXpMlk?}G7x@))cOw9~Ee6+V7+Kuaq@vlW7 z1wWo^9ub!oV`BY!030IJMl7;}PTxuTbGX+n+_uL!kU4bJ$OxgBx7u!+Ua}Pyx&cS> zGILmOu#`9!x7OQ&;C>qR{Zv(bNH5&8vy55B2${FhRe$QC)DU3>Bn?0z5 zRDnD1I{}ia%luUDAg3w#@Wdn>`LkoeDY)Yp_?qGFuAu>ahhn+b{#-lw*FVw=g!(b1lk;v>bt>f)0{}do^*vftH z5?iVDHg?mn_<1+ezo>8kMvP!gZF*#x`_>-fdWOQV3^lld(P^b>eguy60!~Ja>QB9C zIoSGJOPnv=&A``)U#|;LS>g0!m0O}?Gw*fyp`8tK2A`3<8ovqG&Y+6;%jDZvBl< z;dS8JbF{@EmYI$G&|rfsY9wJd(Ni2reV10_zHe?SeN$O0Rr$K{xw+^2sj7Dq9de19 z!2%(B&}QOo$1s7T3F((;e~hIG9i5V|JNYbsuojR&dQI?j_$R4cDWnM&)__DUQe%EX zQHQvZoB6Jd)~qjhK(ijsH;A!W3%0&_hYYS~-nY20=e%2p)^|1yW_=Ky?%7k#I*Qph z-J5?cl7ggMeWj}f!0)6t)Mt7y1|oNv!C`twOjEFA>^))z%)o-W($Bj)o+FwYTZ%1= zq+$yjU}&Z!>4v}Ko$;LvFRkr920R9QLZzENvC7fXl#lyfs_O2tR{9_Xuqm40cHMg| zUEN(jfH50+y=TlC@f?(U{h!)gCVZ)c{*&_c*fqUq{PY|V>xA5U_-KyR7nCRFWdMT2+lzME+q%5P}3f1db!Vzx%KXqt( zfuO?B@*jDwncYltUV>kh%5L)6ZLp^24Pcm{fL4_@AAt=jbX{4KG6L6mr!zfKp4`s` z1@g}G?kH=t3hwshgbl7M8)H2xfktn7J@6V}N_Xy|o`jF&bpvrZRkDErjj+!R&6Nvk zD|w!spvzLKGB8Et)F+9$2g_8p9u6&4^YS&DUPL&*yz9Wju{dBVfI0F8%=7#&C>+ z)(SHtxpJ!CX;!0!n7?*o0oH?l!a#WCAiT?hye9R9yh`%U3i74|c|wfG1bG!fUMYEp zkoP&zH=t2C@<9)MR9nL5#i|0m--f9S(ae^2Hx>*3G^NaCY?tsKO5%#qP`IRxqM>R< zqT|;H5A~TdQIh_xd3Y*Kz}8mQg}TR9`+|Qo#2G}YHzsO^&taPDe7xtCz^(hCHL!4W zebrkn^Xk)QS{o^3-sVlzjU;H#t$stis}R5)+_Lh#X2;P4aey8198n8Z)wew~SA9S% zjlJq^Wlt&_?Vf1B&-C5^Ms&U*S*Y^96J!0^DCp1im}=1aF+qQR{Ee_bAO4#D zwCjsY1RiZ}C+;O%5aL`k^PfFV?VvlkpjV2$cK5 zKX}*i;+6f}mi>r8d2s>%J=!N!=`YBonj*uKWAbTZRimk!^?5I5MlW)+W9hm1^n%=@ zvGhav^uk zxnfvRoo1jw%>5)^$%5Qt`HvUmM#b_^jirx?rR!tqzmbl4XNT|c=Ov0&V7@akmHCuN zwUsC06TygY=AM}VT#w_D6o8|XW?QoymO`>1Zu1-? zS7LsoH9lrwpyIugdJ#&{z`@ne(1R4i(qc(lQMQI)OKBJ@5l+XO4sHeu?(q z@V*{~MU$n=wdNR{4m1|$?VdZa-&~l)$wKp0^;IVqx4fN74=9rpF`a?#Y}j2KEaxR6 z$5PZ7p6{5O$_yxLd6*ES#@emoFv~3huZiK)Qp9v9Kh)r^c|;3`@kdO96354NCbcIz z2J=;_Z49R{?cyC2UR}NS=p2ic`oZH$5G2J~W{1JwVFW>(*31E^!gsNSawp;!puTNr zadJ5GUMdq5^CMb=mj$iJ-5-2{UF*U`_IB#5HIHhVKX(WD!7TvMzlqz6Zf73~K*Xz> znb~r;>;48O%^T3i(t?&-DUD-_p@7TWhICO`bD6Kyon{qcISQTFUute@?YNx1qXNM< zhuAlvU(UqD{S9(BVP4}-M^lv>#^<3P1{*KAsDuQpfE`?sYIDT8h9d%8Hgqo+3S8>2 zz_-Fjl*^t6*>ja^$HorcXQX`5jW}GZGq%_udkf~)t*%7NxTvnGFgMg#it2)g=|X{f zS-LPrBg!+T4Gx-2grs??w!b6JAO+o4nCQ4w3nlPOwGAm_ml#nLrDC2MTQ*H)58Gx4 z&UpX$(QD!dFbj9vbiYQmz^DF8aweBZT&q(dQq=c^QovN=p3NrW;#N>*b&W`6A2fEF zV-Ft*Sq#UGm&!CkZNe>bit$6}mwOgOoogjErRHIJClF>2@N9llt#5Qg@{%?elR2cU zV{T32hE6La{>MFyH7g*zB1Mp($dXY$722 z2bHVK1x#g^xX99Mo9}RrVd~I3gO6W&0zAejgx2qo+%uW#8a`Lq43O-Ra z%v#qiLbJ&AxkJv+laJhxSh||@6pc5;$C0hs$->1Fe0m_^ax%=^rx=L105-niz5_}G zCtLmQ+5tYcxp3#c+ZdB|^Nr$-R=NE)3z@73$#FZ`V-Vx~6+YEBd+}Q@NNYaqzJq<0 zr^c&%VQZ^$i<^9vFJgBqo)xf6xSNAI-ryT~noZVcE*^>6re%k&D! zjCA7d?H#nY>%VHRPpm!qJ-#&7-YIGiSV$VZSw)i-YI5KbQsqjy8#<5yJiYoY>@pJx z-F6pE@OWCQmB-U9TY;mspbhC2F&yol1nNRK8FpR-C&$Fn$HmfvW9cJE$Ka)Sufb50l-df zIv7!r}`=%532l0ypC@Mb^KCwAU?1Q&o>|{?K)_RJ58I47Ea_> z{yTSWkg<~B=j2kM{8ZI*9)1q+AG9pT25H65W z_bR_%*xr2qfwf1MuZqLZ*0dLcADT4ykyqr-qdWke*d|bc>TCMWMeGPbXzz9pLLLL` zU9_7%HxSaA_5z_vYa$Rj=(`aJRmRdoV(Ic&dLPo~2KF=Nu}{V`7`)b}iD8vTLJgUR zX3~LF=B%Dj7|%e;?(c4 z=I;d#{WepEu=HZSudiC#{0cTp7;9yQ^Y3IfA8N5xZch%KP5r&f9WFVa+R5$9{BfAu za%{b4S>ddpd~aYU(^=|vhar%g^=el6RP(3Tavgn?FFlXWVZkAwCy>ZIkFL%lLJag0D2%f1K0uz=WrOX<+*VvBVW( zikPiuQt73}LJQS0w2vU_5AiuEi25z068CHkL5Bx^UqqfUt7weDJxCnJl zfwYz|scM>8(npmY{teCvO)t=oN>&gH|TCGsYyQ~gCSG1iFL$YR>YNM zX&;~wlTq|%nIdRhgKS&^qn)M4%~;Dn?FmWGVbfUkSj#J^wyR1CT6Fs616^z?JirQT z4f6Oo(IGo`aI}`Hwg-+rcS=LmQCNg+FYdyCXMVsA!0!A(To;^?%V&*J(cn(UgY z{&hUnGMW|2VaZ^6XI8kB_%^%bH{bZo%P`TEyGyf>t0UqrkU9mNz6_;JejCdNx> zKBT}Q{l&%;q;q;%a<~apZzm>RMjf&;f>pz6@BuGi081>?;#~p}ROfXKnK8%|c(K2q z2MrO7u*S^_XFLWK-%^cjkCbC_8zb96Kiz^4w1BJJ1Nub_Ztr6p=sLX3v-;>K{-c)V zIjxoP!)-zNtG-P6Q`RZ3D*{*xcQGw3ZXZ>3v8Ue_e|)Vr^BBYI!O>V{m<(<6mPBjL4| zzCy|h0V`&(d3=gOa^T$7@l{B*C>lTqaFs%$vgvEqsS=`{7lgV)k-wzivVeA)IqaM&3;NX zI>Z9Ac}uQ_%#i1q`{tJe`)TH`g*wkJt8nKF(uA(-({oKyn{bIC7gY1=xRFD)pSz4D z7iKY;4w_(CqFg2EDOl(;x7phsOMsN20=Xlo26+%Sw^uNfoP4?(eq@I7K;3dG06iuK z&D>X@Q}=DNQ=~us)>UHrR=QUe?RH9?3y61(Uo&nKjLT;fUB$RNkbxjbw%bxRZ+K=fsbGzBQR$D5TjqL?@>b3>rXu-oxl?*l& zzT8*42XlT?CHa%Oy1Ou>8}zHogkC${^%%i-{mk9i zrQWZ~Vf~m_7e9ZR-)Q1R|4O>oeKy@Es!u;2s(ck?5iE+kS@fD)mi0iUDUtoRb#Z5v zd+$w8_P3LkvF)nS1uYx%Bm1#V)2FR+>N(hBEu;$OA&vTu0)1^bP3D)A8Jw%kW5d=I17mbZ{kP*S6jgPg$q zq}Kdtq#z;vEQr{>xFzzMIMrV%F9QIQ{)weW7i$RZOMt)Du?(YKd}e+6Eln4o&i#r2 z*iv}}o#?ntMcW?X04A<1pmU!8bH@nlQ}!r+jgR@AUE-qu~02^?*NKXFZPD!q%ht zRKFGwgv{vT9D12>AMgA3pK|7g&7RFi&`E8*KYYN%!Q0GqZ zcM0p$f2ldCCXJ4H4NZRl26#F+@QLmIb~|23W%`G3xY%3KB!u!+%9(tUFa9TA`q zCq}mHwXp9Q3IAoMNp|ax>wthrWvVX*XE3v*Q@T(4C<5zj;dXj3UO?24%JzF^+%`xe z7t9llHl0fb{|G0Q)#`bOKIXfV_%}lKTS>P=p|_}coj0j0)>Qo7_Z^NE9s+|&yV`K6 z&RelaK@YvOQ1dLO6@$(|L+X}tu56eDt}`|{ni+%3Hm2t~`zFwqUEIf4;O?O&j7FG% zTDYibdujJ9hfKS%3%vHdNu6m`7tE646aY-J%7!uuO|QXXL(|*J)ojtF6w{u^AT4JF zCqg|5ZHPb2ne=M4x(ls#O zUp1%7SkEgfO+Rv*1_{lFb~g0VR8Hd+oSqnh{Brtb1K?SS3%Hz;IlC+=%rZ~afT9~g zB#i*wt5Pe8O@O*qO9?Vk%quxO9Jbp&@rq`-e%4SL^EAkx(lWA4a>o zn_0TH1<6#^OU(y1RK42#Y&-}nRoPjK-4?P?o{)24_`~$)xH`n>eeSki~8Y-QMx3^2Za#dTWc8L zm49XrI{3uYC>Z3AQ&|gmp(6~j4QX@FdVkWomS^JY7PG8b@Axs@u~gVD?PHVeps{2v z#ygHKL$9ApTwIc6%@$)ZYBrb z6D?>4eUy(9x*>7P@I=Q9%DGv@GR<6tHR!Ad^*(Y;97Tj9b15`Ltwq7%$efYSj_TY}hcYP%sjU9F|kbJj9uyh1hdet7y_eB-p*mx1^_%jq?-K=?^p zIIvX4doN2*wGXpXms&j?s(^QX!KW^>??)&v>*J`M08+wZ_MpF>H=qClKSz^ab3zUD z%-rnaL-}3G;G*X?e%#1?H(>v;dAPj{0j&V*Ei(V4US8N8)pCeT z!V66qXZj^>s3}eo?YkN!G|%PC3qz$p-Lnd;hl7r$Q*`rv;65@>y9FxF06VSxvkS{dJ^i5d!jY4a)6JxlA@J2DBgj=;Y z6V62A7|yoEY%6AAs%zk=IgFm*^>R;y;f}9j&Rj7BrFD*W5S(KdZBn1D!r(bt6OiL6 zg2xQoe()S^OLA&0hbYP!LS90js}?lx!$JBKE~uumH5}nT4$4flcTk%vK`qCqo#@8X7RJXt6uY-LDfPaRGssghF=EHne8*5@tGw|1z#@pnGaiLkKn|&tNT4; z`gr+~Gmf^oNuBj$ZQgM*gUzA7rS+^(ka;7rUtG)u|9h`$2{rn%j%)R z#e%XL=ypkgn~tepAo(l^IqIEMXUM-O)6~{Tuq83(m1;y&PAV%JIG3vWgYvn=@5b6x z@iHQe8v&sRt1IB9-LZeehN6eQF{Ewv>1T~MD>rc5s(Fv@FZMea$DfFMG+5tr0vKhBTNhU#rC^) zON1Uts#3uNN!1u6HJ~6V!ehxy-_Y$HyaSmcOr0ItJo@ua6PY#5f42p$rFS;?Ovhr& z8_oYw1gdU}j2lPIBkcV$s@7i=wsn2pcD<9u?pc})yy3AJZIgWuBF6>kMh7@d4BI~ca}CM1r!bVjhZXVdkrAIZ+^I=9F3dBl*r z^=R$z_4-SyNw0UvS=&RVaO%MquDfZH8bD88a5F|xlO0;vlMzgJJOX0$ ze4ytOv&IezPjh*lIt+0KjN#YL6?!Hi;0llEjB$l;MqHuaH)bh@Mn$6{~JxP+~@I~4*9=_ep;Ro4SOVta{81`ld zJyR1@(37(am_(Wv``~$~h_4J8NAIRTFdKw%ET9MQc8(&S!#@VtM>{FvA3eW0LU{rI zDBkhWD+UyZk31DL6!4D4`Xpc)_ukbrjUlHfb|&B!)5ej)O;+|~94jcEnIo`d1uA*) z_zAx8F36dr(@8?jp68A6jcQi7Fgs}PvkV2dsvn;H&2xsrxvDQeaJA-nFhDC9up0Fo zSp-Ef5Q>3MgjW9r+)zA3wNac`G6gp3Pmkyf0ui6-)D!;ra%Mhrc90n|kXlcXHfGFo z#BSEk9zEvSz<*L5cuj)^uZfKYuN^07^=x3k7#?OhJ=nlO@K_Ok%JYLyMEqbeh48Hk zKlmZR-Q)b=JI@(5H4ZG#4?gcR4Xxz)!AE>%iSJ*)5B}aVV|X`UXa7xp@DpvZOfP;= zK;w4j%&jLsC`G_r(DZtE!CBhqThg`M?W97zLY_CT-y8Nw%=zWRLazMvGyIv~#Z(xs zNIUm5yK`r-Vn!!=@snk|>qg1zK6#2hWDgs>BmCq{+U&(o{-!qKCm)KXm&ela{88lp zclgOO*27O;qdNaH{N!g(_U0!w{*#Sbd7TLob_sJRw(_rLfseLH2A^hk?Sqb`AZY6n z$PttxBv`VyTr34-3ly?xk$#iXP1_I)D#L{lVHiDrSMDhiUVVKP$^! zrZ%d*to`Jo%y4A&C1Q4;aK~8{p|~z^r`|(- zW`LfxG05D_H?}x0_9Z%|t4F;tF#=N$x%@(AxhrG(+DI-(Sf=Lz9=5pcGQap0W{K@(O6CWmKyd_n5@Xa{hov>I;iUkrD4mx4Q+89;zL zb8j3GAE%&6s5bWwS)Ye5C49f}`J(p;r~`V7-gBw0r+obN%HNMawqAT``sMoc z6Lw6H{y;*-NhVaRBTwl13D`3Q`dJrcCV>TZtk?{GN_cW}mTHREuV0qJ<%{&o(|kQb zujrRI0G))mGf*!N`zq>XDVyY@*VN0a?>Ah!fwaqe4ajSkd&v<~DB?~t`(SjiCPatW zGenXR_6+bT^hWrP&I(fr_YF_K2l9OKq<SpmHb z*xLvnu7h9;^p)2x2;YkJ<59i;>Zr2;xy9x=yViaJ9dCyug9vMOa8Y zbV1ym+hUtM>P-RP}0Aauz=riqT01&+0X!YI6UIbnc# zt@xg4^N*_LpiYRKmoydbs2EcaO$pWlOaE6-i#vT69WmPpMGP)J3nt3Lh2X@`SRhCG zdGi8fPe2LxtZ1#Da<|cz+6Z1UW6NmPBqQ57^<9sGUh_b0z+H9JL5ayf0Ve8YAW3Y} z&r(&7C$7B;w=OkedZEdiv>ZRca})H;y|6?S)X=qu>EM7RS{l&Txs4_;8yJTIvp_yt zwP0w`+J*9QsL*4rLzMWx3#j8Ouzmo)cJp4+^0S&p&uz_Q7GQwNY!*XqD?A^W55IM2 zDtjcl_h%z{chjixe^s5C4B1iSAkg61}Vfs(4+n#nA;r3}-X#;Lte@;Lz2H z$@0jYD`6i8^O(7^2z{+Saz7F(lIe`0Z+GbC~2I5rlIymF~OwFSVgviZ7$} z@F+gSTv%0nN#qrvtJ4yGugD>OZPob|iAmq_Xune7u+jQiA8LvQ7h>zGHxZsW3~(AB zVHDo{ZzGMw=LCc!0a3z~%`klpX{Or{dp5rgHoI@m|Ge1+?q4|CWVtU#9)cPEaFbp$ zJo)Bz(0wyAn@9H(j)|fBt_5twFQ4}XbN=o6uRiA;12!ng?MB1wPm?Fn z*yE@hYr4VPAWlAr4!~XVFdL4$VcEl0mf=m6)6qej)08{!!7%@cF#n?z`NxwVikvSE z%!`~s3)G$aN0giUYb;%QK>q98KgsWPKIG49zts8fr?|)IErc#njsF>a-LVgSzWnKJ z)TEoctuN~LS!avhq_eI3)Os}7L*gXxN3wOyF$K-Lr7}Gz-aHF1f6lVl(Hr`O8WU@o z+oTqeqh$`1{Sg^g?yz34gELU)2%0k?HxOVy0g@;sZD7xHF}|(l+bF9G(xt=17>F>MzeXrzeydCO-gl|>=2ur;qPvM5T& zd2fVP?Ib!FB3zknWPc<46=(%{7jD;3_RS7DdFX#fsEwf>uLe{fuvf@as}i0gbfod9 zlb-#Ikdw!XtdZ9khLY^RR27Z^x#;i9qBM6+vB$e{1>G=d^7|wv$oKmFfGxb%piLk3 zcd>pc(DCL_I{FBu*Nv7lh^)y`wV6>xXdtyTDw;n$U29A2BbjGA7VD|gWaa<^ii8G9 zTUTFLOdu}J$0u{T9~GWEv>6A1$ZNkuog8;{kmg22c|*2rZvDL1`VKFA14p5D!L2-% z?YH_Ax|5lz8X<6!WKo4%KfJK+PJRyh(mJlsMlCmUOQ5`=aHTt8Bs^i(TVP?`rlMY( z;6~$qJ4c`+ct3>o)2?Rp%k&zNlOG$1*BasEdi*ZApOFJ`YS#UjSfKd1i( z_vpW5aQbg;2HJ(GaPU$88`b~o&k|2N%a6H7|8ILK-v6VTh_-1_weO&R4Vd3$POKyZ zP*DSpTe3suGrR9~WTd7F zkEbUw{k54Bn+HCnkQIkS^P4T={`j+up<3D!U!jIUb#8eVVkChPImf82x~zHpQ<-Cm z7(5}D59#`z=N}p}!|ZNyWlh_dBD~aBCll>+C3DCc>`@X?KdR=98$>(h$7GAZy+h`& z9#&Y=@^H{((EBW&!YM&JLFcRHjsJ6P=9n>P69UW55N6DA?IcF-&&M*q(F^ul3WKW% zwM6(M!WaNN+wRNl|06S3CkfS5|s3dJnk6*s9O9$#3q*HKP((Cq5o^83GsHr>^Ig#?2= zDYcxR_u8saV-l0E0Hw0ISI`4M@M@+cTr5yQla?nqTVnQkvi;5Z8AM;k^*!~Vy65{dW|Oj%kJt(+3B|dMq@Tr@fpLo zjJ)nynoHMFpxL|1X0M@eRi?wZQ;#h{UD>8S?kUa@!kr=D&tzv)k`P;yoF>By>8b;o zwVJ5RuWL4_ON=adU5n6sE2Xr5p}nR0LE`UtpBLd;1)CyZ*Sw_@pshs;kOE)Ez@MiW z^z#%0ex72m&yxn4dz)sZM_*lC*eB6(7Q{rDyyIXc^20T^U!890ZtT}%dE@C*?o1B< zdk#Rm`ga>LdXE%9;sOKOYSp0rDI*(5>bM$p=-$%F<9slBJM^h+7GSRjs^M4SZx-q($?{)kkOuXW; zQ!r1U%fqXrR0u%j^I;?uVPg9Iy*^=%KS#g+_#a^mLTVdu@HXR#$#^J4#eZC+;eR2n z;r}t+3GPL8yS0CiAQEW!JxquFhrHsTwz4;K5I}sifFYul#@GG~VX`hEefVF!f+3QR z5PAsUcYb7(-w1FDz-4Ff0x?lok9P(ChyO+^Z}%WlJfmVCVsqI;&tD+}xQM^+XoWpzE*pnTvgq7A~RPZ$ZD2tY68u5o|UFIr%wBI94?HPC7&i~)H*%cw{ z0Y-KWJV^IWiOJ)^i2*+gzoli&q>8dv61KdFv}@WhoOSV=?)ckH>gsVoGDc-8-O&7( zjrwvAzWm(v+4aCLzdnbr?q85Qjb6s^#g~DXd(imA;N_>k`-1-b;D9gRpSwPwKYwP3 zQGY_F%-aIy#V@#4)x)91a_D6?i>&Ww#1A4;%IVMg?w}0M!l_ zCJhJ<7VfLy0HNfTF#qx}|Ch>N`|JF{LQs8fhi^nD2Ho36>Ahn42lq}vrh513N?zI0> zHB-gj2L~WnL{m4wjK3k$+b}z&cXl^vG^6@DE~k&Mp$CUAv#*a?Vs8xF&2niz*}>iz+wFe3Kq^?P zS=);&t&g^n8S>9uUjN!yXm_prUKo2(Lep-v@C5rSxT6=W?dC^lcBf9$$g=Aw1_q8z#qKG2K3T^mXL~E(>%KB_OtWAj>O>Vi z#?KV6e81t5?4Z+r?|b_q_`-s0gZ;F57j>*)dO)Ld^qZL@RNs)EBREcFYB<}tq!O@} z(H8?@=8W|+v&~B$@Rzf;Ma|p(Vk+CO^9{V}WCv}41#T))`);f|;T4(0glct(0yY`P zdEBMS!L({pQ&gqg-b2A?Ew~)*0P;4*!+w~dpWrIt zXpHNFHL#7)@m=PKhzW%psO}@`mmRcVPrqDmGp)f5o@VAEZ`0oKh|WYZHcbsx5+wie zD=n;vmLq73oWEO657Ba<7cFa5w#PmDSSY&In#rvcF<$B zek48`DAdo$qad`xMTz#exw!z$AGf=o`44}|%Kb&=?XSyLDS!Vt{p8$SzqY^kZT%1= zyqs5Uub`)XmPCM`QkmmRTEBlNnRk$>%b@q^W!sesX8kjgx!>|%B=69lY1^4o$M#yi zy`299cjUL$(DRKE>isi!{oRGZr}x0Zvv^{*v`$>$jc&G+AL>0LF`7)_E5E2hVEQ9d zgvn>#7G*Z&Gry+8BR2~YhYK;-<~g?bZHrQFXS>wsfkEa>ArTe@^~aEITvSY;;oh

IxO*FqhmF-y=`G{`Gxhx3l(+;&`!iZ1e)C1o7ga` zLbx)9D6v_<`%I6EA?fA0m#{2L++!6?+#uhK^)e@?Cdi0KyoQW;x5l15>_DY&^XWa5 zzR9PzQ+kR|CzNg{?H-0M*#KgQAvW9j!}>BnQ~ zZK8B|k?NW#EBDsMJ^j_1{Au2=>ZJ};G?uBH^BH| z(Xf(&)k6zmZv5?op-Gt4q=93$l^Myv$|E}K>`bl@JwTHSH2F!e8z#RNap49US0XM%h?HBq%zG{bl zkkTJb>LZ`&NClZH;WKAPnHrhTWRHhmYJ5I(M3kA#XO;(Rqhfgb3b}zIUN1Yjhc-+%H~=&5e(CJ1OcQ7mh@zz{Y^6v zx-Q7`fivyfe1y;k{X2qZ%57sRO^^rTcLXs(3HVU#K4X>}vbU722-;{!-(t=r!{D43 z1#QT|q}eD@mwihc3fj3$`9XyikuzNqF>D&5ebBacXkx-2RaM}t^8R7g-k=0Sb>F8C z3WsXmel0-g09^Q9=ObtaK|=Qn`tfe8ANIZaL8X>YnYDVKE>Tz38SCaY)+i8Z1M^v; zWewW+qnK9e@OgedllI<^wN&XZ8moQKwsw=mg!_&EebB19pSbI`tUEB)Nk7wk-yKA5 zZ7gX>-|b@*27S=IGxfonY5G1i27PGAwg>SHuQLyn_X*4SFsOqWGnQ4LzG{#9u<*1{ zs28!lQyCwo)!z`mT6>Hfjk3X9NPjQLu{j((qzpd))F4aC?j5k#1vxV{7%O&QkTb=G zM=W=H(@E)Vd84}v8NWgDJC7^DoS)z(QJbl!o5YB}!)KK%uB6x5tHxmd<_0Y2Yo5S6 z@nni0Q$@>=%h+=e3FWfWhJkO>k#Zl20ug7Fcr`VT)=;x?{XTdb{TQ!G^l`anl80m} z+i%MUw`Mil%e2D!!`!SSv2{aP39*3noqgDdcgn=p6iDnTm^3e@zD;+-W*cCncrn>n zcyj2^*>7Uc2Ktd{sxxq4{ofr@sO*%J!>!lXax5?J*|FHtAr(lK^kOr4p4Yb^9ODPvI33?UJ9u0KJXH zxl4Eq>|ulX>F5tt%y;=SnfUijeD6_k?iQIfWe@vw863x@bkW_J6)`&OEoFRLea;@XLg|x9hurZm z`$XLF(pdVjSo)<{x+|7m9!ozROHYfX_lTue7DjD-5KG7V5O4R+SlLDS^upW|`SgO^ zKC$%qvGkMFZ~g+aB;nj#p*w4EKFn_PwQs{0dF(cr+*|Wdj3WhJTa3Wb9zKj$} za89cpvcV1>w;Js$)J+S3n<3pQo6&_T!#MD4J>g9uuP(T zI%~Q_@Ag%qj%GlOtkBy~%tu=CkD3C#HCZP0IwQ-nSYC*9Pzz z5AcDHgxY7kP@S93B$+-@*E5oW82P7NNN(;m{)_ad9=m+zzW*Xx$cs(As&&lkpJr>L zpq-+Sn=n`iskaU)X&~C`>?hHFkV-WF5I%>~URL<9&z^+BU6z6T-8Dg|`LMT@@f|Wk zlK00?@-f3hh0SwOR_^v#`cBgS{(Z!;#uZs4j=VqGAFIajd_X<=3iAJ0RwX4gke5PQ zddUAB>y`gw{&T~-HDBmO_mEBzf}Xwp$H$t7m_OdH!+-9k>+Nsk?+mmvaJ7QZJgcKs z0K~_jL1V_PN@&_T^m$l`6l4lMo!6RmRGNCos4n~m4Bp@X0X|cQ1>Obv$4o9WG@Ci# zm|0q6+bjO~NNdf+G*T9hx|o-;`{Ib&zP3|!o>F>^0S=kQ1gB?>BZ8nNG;d|7{^w6Z63k}8ER_ztyzON`%HC+`pU&Z0G zzG3KAbNU6Rkf(T{)P2vtrUp;d+r$kWs#5B#d$LL;rp_&1Jkc^+S~Eu`tNJExvZvg| zn);nuZ76oFGtW2Fu~~9w^Iu3Gnb=0Ar5whsjfo6LR+qccMpod*we@<(r3u!!T-wH^ zc?!p+dYVsTT($^cNcEg?-T6?Dap~>9H?Eq@_~gJB@zVUaVjP`xCnmjz+-`iREw66V zo)Ao{KSzesM z%;Y#~^oEGRxJ5UqAMPriNZBDI;im+-qjYwxhjR+_hwiz){4HNVG4NAcI{UB|J0XjTho< zNGX-_RY9Kr_`MZTSLpSbC*am`lbfZ-@?1w|mgYaewjrA!ewtik9x`*Pt+$>p$#{mO z5EriEXoQzX^Bbrgnu0}+u_t4crARlJYpI@mXq&p*T*vn!g!vp+sEkaV$?F)Jcm&ud&=cf8b{}F<#XW!Vn{Ts-GziL|p}MfL=kA6jcp!LxzD5MAL)ZkU1b% zv^>bwLRnI7S~WBJv+JHX3D+WegImAg0C(^O_k-v#qFnx2KoGGlhS7|9AMeV|P zJ;n8&@k{<AbWRCUGa{^3?jFS@^{&+JL}xAd8YjVjdV6|2;E6cIz@`{|NE5UxSe`Y ziUSJ&1C(OJ1zb{j62*Yo6ZEtnJd72~oz8!G`u=}^RzEaQ59u$$hlH@NmG<5{j~N}m z7GgLh0Gzg+O z^&sQqR$Q}j0lr3#@pvLT0*- zVLcP(sdB*R1t8{WZJx<9md#Cg81kB#V*~@cP?oRBuExJ&HbU;XKWo{C4behXEgN@W zeP(=~45SL>iONc5h7noEH50ZLBj00~Od0pI@(aI>+Iim?FzhYOy?IvBNq@M8t3L5- zIBbm6^b$D>%*uKxDPi4?Uka}xoiLN`2j_|PGb?3x?&X%QQ2w`k+JtLv(|Y#coEEVM zCfve&y{r0Aec>{lkj?W9@5Jq|o3C^SQ6Oe}y@Hk`8GBm4jny`1u`&n}m zjXv}3W@Aeh=hc&mj*B%0jHe@$s+zN8R29fCJ@MZ;fJ|2#WP2LbB5pvKm{j-1$Ha;M z?z=P}xU0*|5+iLNa1+Ru!yUhdk2aMr<-AT=Tqh$L&5t|4#DJ-)n7yLOW{bGTE?0+b z?o(CE6YbNueeCvN6uC*HzNooZR?}RO>}FAk`%2Xgm8n&?piH!6s`doUNrqB$XZYsy z7^oGvLq%bC_hmG>nX1VSx?-#ciza3`_eZ(Z&mOkd1E{cnq1nwIwxiNN_UX-)zSgJv zD}5Dd^cnWy2ltclJu*VQckLT_y>~7ZOHYWUX9sBx^>d%((+hKZt<0AV)<@v}?O4e^ zvGk^~^i5GZoKU}Hxih0TxzA$hTVm;JKpl4LA>SXen72A^xtT$?9;NNs{ZrT5er-PL^XIld;e&YlO`q4k#eaWRx_3hE z68Sx*b|-SJ%v4X35fWjzb&YVb{+pi}I!4zC6MGhwNeywfut53)j5g}ipSs7!BAS5x7nE%ZtDAlj}iL%{HM zDD&Rv+qHSf$EUbNl4$QTHvXK(`oh1}55Ax*+4A~u+16O-^Xqws(bJYhWbXGgmSzp< zv&+g8d!8g?B&?)tn0!KPi+;7w%9KZugwve85Q#R(-|`vqvs+EQgs(D##yzz)m4LNM zT;B^`>!zO_Q}{4m;N!o_P)ntIWIy(FKD(y%>N0M3c2qG*Xg_Pq3jM9eA>0tq{Sx7k zS)hvgs)dP;Ey?KGB(gWF8~B>wJC#SkE!zTmy`sTmu2Xv53DD?#J_0oiguK*Ht`$f7=(gYf zo9&@L7jx=12-+)6+*8u}Q9o#Mn=5SnsQ>+1wRrRJo3@V+@6mpFkM?i>>f^n3Tx@F5(V65(mzThu9cqG?l*LNNGfs9))R$mbmE5L*`gvM;bkt+ zb|#Xg8HZ&k*?RbXiT3yTydl$)tWW<_Zzm2ioPuFv&Q^Ji+j*>L0oHlKM0PH=KYLcY z2L!iw(QfK(qGNO6oVG_s2j`(RXVjcobC!^}=U*@`lOba9z^7ax#6!I*5vwH{qX!OK zX!0F*%Q?PkEYMCWE6E+swhiQn)60?s98J>KD;O7puTTF?!Np5yz94tBeOS#U@uw0U z8FCnUPUj+l9Ig>6h;5FsIDl;nkQ^{^(Q%jbEdai=Uh% z{*g?7ckdAJfMSc6*j&aYE&XTDk({|o_I9h>u?R%Dd&zSreOH5Bp4)*|J$JD%*S_3y z2zh&R3*5+!db%B3|8{S_jh%Vxm@1HcTBFLGUx64zWW!Bs5;rVt$c!u@0DH&V%^S;f z{EE?P3`UP|_k;so>3)JxkirxZnwv=6Q!u2k`H&%8cSM!Lfp4r~XJaCPG)&J z=TkxVL;KMgP5nPK;caU%n;mF%V7s5n98<0JuGZQCXe%*_QM7vuCmYHdQ;A{B7zVxM zRHw3A_IFdL*4t%fw<>;DLwW;Z<+lB)%qRV@4UL!?=So+od=cR{+_@(RULLfWNoCvp zbpSJw{6tFwB#jguZt^N$7vvF}-mGGm*)wLvz-f5{9|N(YOJvwOah0Z;@z57Mv$IoH zx%1z^4%~%zAbyurl?f3D!xUKxXn2UP<98ZvwIn4Zdka{<_9OygI&QtB3K_ajs> z0H02Gt`@Xpu1caX9w}YVYVvGN~&+sap{9OXr(DBr@OoX^G@i^cfr#6;<98`44xBBh4*#r0)8OO1)3$<+6)3u#Np-{h@Jt&<@JcQB$Pf3OErK;w)yv51mK%DEY z0BO{(nKhD%Qt4+K((~OmczQJk{T<~2EaLcIu{6P8_3#_|$bN2rD$)LPlC2+Yn7CH^ zK9$|%g-tm18MWn)QiYEm-u;TZ9YE5a{58KCiG?B`p8f!(dvSfXu5`HW{8V+e(C{iZ z&VSUhJa+@TNb~K^56X}FGUX3nr+nxwWEgPlmP2MFb^^nPX!WI3bB~@S3{qQG8KW>i z&`{bQNi`558CVXO1j8Gd3t1Q~3M|Nhd}fI5O-QFZ{t|zm_Ybyy7cy}d!X2EJWKSg~ zZf~Ei(fx*GDto(MH5eVOX|Z3sUk;Df?y~Lv)w(r+pW#SWrPll@wsL3uFj%?k=tQ`3 zt(G3c%5^ty(|SZf^XBg4V?QG5bpm87DR1>u}s3;?#i6nSOCYq`! zE>v1vt0L8jpcRNF5yx>5>rz^+MXjyYYPBxlp0Fg~in!n|;=;WS3Tg#JG4J9+j-7&61$1{78_RWCO`6YCv2d=cW)d}&~S@sY*DIe@@Jx}y9t$^tH-d}R*hB{6{Vr50rO91#ZD z1FC#e@IqN%2j9<~Y`~Q`*@7x@tia?iW;DV25nflA$3VVM{piyz!NdDZUR443doIZ^ z{#JATSoquTM)0s3FXO2nJ+5!Ks#JKoyLsQ~ZKLyA@OeTjbtUX0AHjf|zS%rc!%nhv7Ch&4%t&(h6Gn^de#+)x*8zgdRH+(<%nD4jFZ?~(Bm?}F zP~9#Z>z!Gv@DVLv8Vr3f>sihgK-0+FIFTUqZy>m@$n=A*n$V?$rfH$3t3Ela0pSFT^za7Ri9B zNo6{aFWDY1j-?LLw^b@#snUc5Szr_;=|^K>KEl~b(K?V%CleJbt(dQ6`>QgBP5lWX zr41@`nw!i^DR5TxKUOBfIFCBvI94y=EVQM=ifHu%%-}d3NE0;v4xSVbv({s&1In}n zQQgi#b)%1?M7!u=y%fUj4qq#h1K$m+g_@T)Phk9JjK4PwAYL}YB}Vt;T}`)+<#W0< zBB0yLt7l4`v{sW-F~ckrQk_8i8Z44Nyh)YLfCSYfan!^bw%q$2+Jn~6sm?>i&GQo@ zRjgFdbfceLIIi)_&Bq<#cC?-g!Ev42=AA#-t>DY$vCR(h&g)XoPpK!&GmeX;A2$eq zr1b%)m#H=DhY`f0&p4BH<&$QkURK!=hqV_)3jA+CJ_`rMOSx`VYo1Hz{1a(f{R1I) z?iz1n{xKGQ18h^6^RY{9Z)eyqHlI~NcaJUWWn{nQo;J|@qwRXN^G$3wGv(N7veWJ@ z!57ke6-+~K>BSnGHCH(;Kc)s>e+-`%_Bq>NHnM1KupIjg;sLePlvOF(h7ek)?H|uq zVhzN3evVz?^x*Dn9ljLIAAVEa^l71+u8%Ob`QBCruy}8x^Sld6 z!ETl0c9w#s)dW@>Np)K1sAcD{*3$jw{6gC8a<^&8H(ukEt`QY9nN6#=cP_t~uO5*- zkPn`G+kD_{3ofe?XqnF4OJ)iV-QQ`5PFxu}aWCr+VFe^VLWZCz@v;FAcUNUezT;{L zd&b_LybsRV_jp&75DI0F)Ng-$b{$bkL`I&&yv@kV44hiW3XK>5H7)+8aeM_J`#CEr zma0dWsT&bbpN+;=$viq&+(N(D(Gf|xVXf-7P%Z!0P@N&#BA-}5`9o?CpsK^u2C9op zi!$m2P#yF0JXH5eZv?7?=uZ}^XT4>i`uK>AL3K232zHx-DqgbzR4p_wD9%C`D{REO z7kx4W)i&0XS&lND8K`#m?lyT)>=1>zS}ixt8I=`UK=dkba!9e6Ma!Q}bcCt9bF%7o zOQ$q17zhDzFfR>*MNGr>9;A9x^pU!4Ehjy!5sXx4trF`Grb@HU@)J6`JSMvj6FObP zU8m9|%A-PS2FiBXznu+yyI#}XAMOTmOb#93b4(oD@p)C8Uxq;W$1%&;W6pDV25J49 z7|GOi>%S~pfBM(ux!QlbMzuHciWTzfQg1TP{!LV;{Tb1Nn>q8W$wM*eQ|4W9wP=E# zQhmz&gITh9`O=mj)1~}~F6E=t>kY4G?$U#=yO9m{%{6D~!B;4~_Sig)96dizBQFc3 zPYk7xB)tK8iEF*t)P`1mq3sjqXkuwW;s~soIF{_+?~R=PfvZosJNiY=A7Psyc47~A zvxbQLRv>x{ioLC%uhEg^lR{E&Kqu4PuPHS1P@1`O9eIvcKd45gTa=Zl&3P-2<>DqE z9Ki5Hw9*`lwxsgYVy%0bf>WlHH+?Ej!YdCiA$Hlz98AR#MIXEAmNU)I^`rQaG}Wp< zPbtW&c8u>RN%Y04hA6&ue&6$U>c%6dxQzh9r#i$c_t(I4t?*W^25;IQ0&~wppTuLI8@(z&| z(`pb~LUbfP4}^5a)8|eFtLk2}L}NT&$i|f{@bNm67*}}ff_t;wbnSF&6o2ujU`fjx z3F_HQ^?Xie>bPv`VZaJmT%+j=`o@Z7H-I^ZW;}-ToPRdZ%i^~MA}-?UU|_9snx7SX z)7`fw)N(xipuD1SY#yPX3D}3+61o*XMOSj*5q{G z^N!X8^dE6QPm0i1;=L8OmXr@Us22X<<%cNJp$fZX|5^3e>;PEAFYOj|9q-|{pkG~3 za2HTeAxq|(F`22CbB5Dg8kB@0m}@VO9cg0bwG^2Algo;$Q$vgJ=+zkOQez@0sq>(h z9QY%{y>#{h9?;M9+M79nVM$Rq#33^^wo=Bv<`YZQms%wHG@W2R9&gkSLA3RwogVG@ z5KLB&$oTsW#Ke{wO3<6ysTw!oXcy}hE_+ZYlLO2!uI4>LNy=zo-sKNHkvkpIX>x<} z^tIC{M%&kq26xIVkCQoTKEVU$W9={?r<7&N>}zS z-=Cel)Vb5@kV=cQ>%p$rWHowRE%ljIZOqQ2Kuc(H1SrIlA4$_+X^xR(Kd2OuW=%{b zCOCbntYb$V>a&k=~$G%e@DwAa2RLS#PoA&?f8wS9bPmI6x6-c3S!daIHTgb`Wl=c*Dg){L4^G3NZIcg|_*m1ztjIA-ORasV=* z3Q-b>;CteBselMxBzJ!|X~GmZ&9|C~=zc3#@=~ihnWu&7d6wx#D$B9snj=I69=C0@ zj8#*HIP7EcyM&;q6@GS&{u`1P)7|HP$s|ag?Ubvgc1P3QxB7^S4d&affE^{m#k9!1 zK{ao?msuu=Gr~Q+L#%13P_))vR!g0)5nPVx219@L^DPTfMN`F>TXr;CssZYDKZX{9 zp@OOmm7TUw3lA`2|5H<2Bd@8iUiD1~)d!L5Y#gYYR7Ik3Hik?IIV@5<$hWHWVbLAi z5aSxYSmuKKKQ_k%05CofOkvEGy+$$Qqr)_|}Z{=k<}& z5!9FNd)Dgh3jBkCt<`(XniPo|m8vd^r+yVlw)CR^X0LExK#^=iO`K9jilc2qyG7fa ztG8G7Jbbv^Y3#zb!urq@_gNqo_-(oKbx_y=ixvba!sgMg8LSyex<85g;N4W zp1a0Ub!dXQgOosAj55S?^L#uXJ%kU$(`-$zlF~miv%EdTfAhh1@)1Lv=KGjAH+4}Y zD@deb!M4NzMz}aigYkr)8ANV4aE@8L?rGu{dFf-PL-=urEo0#x-!O=S+g& z(hr(yO%RnQwAdcr8o~;-ock%>s~@+MRd>9}G965|dt3i3SHe4*(%8Ee z#-gt{Ew$j@Km#f3mkJ82({U^T0W(FI)fQu)I(D&@*s@|9Y#hqGDYqMpFfDj15 z>HasU^+QJswOXcHC<)m9v|yfsYTe}ch=j)mF3S~|TV}vNtfGgRaA!OtK=1zJ-iInR zE&axu5;N?x`5IjVZ^aX^UnrlIsHNAp`aQEQ4Z%lNxBS>#KfxgwKnlcD)-*u(N zuIYhRV|rokN6M~L!}2W+%`=2&p4sq%ed(|bELg7KAPBEFlXw2xq>TNadjYKClZv67 zZHiu19ucNjvn*V^k4D+y=Vto*p;tquie4GkwLTPbg2uB?!w-ic+voU~{RF{wOaBMv zum~(w3_HhCPh8TKrLF;j{y`wHkpud*`qx3Na`qk!ij~3?_Fv#(fsBM?lx&GVts#4(?eq?{^V~So&e{2Po*DD6+WG5K zv~!+0)zZJmTy33qdMkg;{`bw8O}LHQe_};wyj{lG|At|9j9FRmNZbDF9l3GF|7H7o zsr}_lg|$cK$(OcHk2X`s_@&h#KwCrBL3J2mD(Rl~Ta6Kmz|jcKFEcZcph`n~j^4hz zF%Vs8fQoe%_M4yOLF))keFW{F8k}#W9Me!ALE84GAXQsAr@oZS$Nh=<=MudilYP!n z(s&z`^ox-~`Yx38UMg3AAD49JH7}7eIj&MdDJpDzabWb43fnX*;vKLA8fo;^L^(3~ zpzev{K|LCFahoy-h^7kde0zgc4d)Jmc&z3HAv*ANxw`nMLvSFeBAe*nQ1H>x*7-?cRo3D|PUt;s|VB?ILw;AvJsiat% zS}z3Mn`M2JDscbwuL6m4JZwajf4aWe?O@}dG;g5!Y2zn*FzbT*v$o>d{vSf=J45M5 zLg^u)^!!lzj!=4TDE&ex{b49QK1j3e@4xv#z7L~9Wu6M97lhJphSFXry*iXG3f0*s zl-@a%E)S&#hthBUBj2A}Lg^zyWsW7ip8Y&)zs4sami$I830TwRjxY~`XVm9%p-VEb zT}Lnt{|F74gT7-CcsNSb#xdW1*zT%WF$1u!$CA$=3CHX%an`o(7Cz@q0VQxGCNRO+ zNzd1&&PdqrJ8E0rpSZul3u?jtb6l0FZl%k*u zwJmQ-?v=BV5y>hqg^OARj3P> zXcR|{S9pQ>6Mrl^MEhg;(Cja)Vt;|7&g0zN7`(fmN0E)5GJet`Nih%_+5(VAAxFvZ zqNkSmld62#{{wn^y^6&)J04}^mnWb8hNH_MzTL%&kccDIy-i#Jy5!H5yN!F*9DcxXCX}8ubEww!~?kO;_B3 zBg&nYhm}&e&Amrdx-*}B#{P5LzTza)--)7dc%?g<>zw-B=%#mGzyYk|Dyd&0z6b*z ztSyI&5R6{nrhDN6eGCF_ze@VKl;-jzpNU*?6BqGA6vB*KurK|a;e^#dg|p1yv%pputj^BndCfL&P|oYHdBqjX31ndOs7eikSRQsX8q{SBA)dUxoTPU(^c2V|8%s*} zQ6Fwu@WdEAT(V!J1I5vF8(C5vQXn|$4=l&dHnRV9Kaek}NX?v{yth$MN*POo|<2EZ-Xq+M+!>r?U{C&1MY% z(P*@cj!4x+uu$DTif4%e)*{6l&d|sapG%^Zi|D$atWyQ=F=*KYmgKhqF zTey-R_K^?+z1?}?zi0%)DlvyM*-f0Xk9v+`4_Fiw;&x3IFs$OU%s{<&v?#yK(>&_fyo#rcHec74o^_ZQx1qYd#Q!SyHdqlbEy6KuWn$lZ0S%)BL zuj$a;dG=ZJMlo|`MFSf+-3KgEy*TjJO_Q69=1Jhp&XcCr+qpFm%5wAa$N_e;G^Bxs z#4}m9n@j^8L7o^|fU`wTxNv7T-TjOAu(^tpvfTl2ufB~p7*rMQ_w;^mFb_bQy%i$& z0MK-=Hs_)0!aKSp2C=!kE!X0G>z9)oofKQxGV$s<11ShJdBClj6(b82Dq3z zz0;ng+q4I-E1Gm$78x91(eI7w$LM-z=1{}ms++#*8-agyOZ-I@*ElurS#+B8La-kkWG|^ro^g)b z)%JRu@M#^pb=JS4Zn{4NkV!wV4LB{kFbMy1GR-)G`1IFn0sEtkQKpglsIYrfuqUpb z|1?Tm0E(-CRBP`Cce}1nG_Whc!(c&v-w_50vRnU9eBf z%83V}r=l(zJi~PbFPagt(J$j&Wg^K7bK-?7W0$r|^zzO^*Q|-2z0af+q2Y%g`enH&7R(mUrZ8XtJ}0hhdz^|s0TkGyN}V7%`cq_Oo`e{4C9boF7m zbeX)h+|_v9oqdhDy*+xSCRD-vsBbUxmf7lM~>ZnwGivQFNVR=jN zDZ!_E<*Z_-iIN1`sk>FV7}MLimwtNPo`P7@aF$Lc+XVB<{7ksUgH;XDnD zn!IJMg?WkdP#tIC>g!VtMaG`AwA#j=dHPul14HRsetvW9U(M#2X@81o-$o&;SiiZR zx#lKpv^npx=b%5PO@9%C$oipcyxjl9cJMXY4@%X$GKIH!5vQe<=256OHppv;8f^#J zbDdV*bqBczYD4B!t6${WBZyP3`V9s3F4>T~`w>(9UD%FcgHazz%c zInTZ(Ti+X>Q{StnI}=}5Kd3if&UN|L;Evat$ z5bjk(m28K@Ir`r-7AH4Z!LQiQTEVB$iYd>_8}w4;RNxE>^jj!%`i``1oAMUEOC!%_ z>A!6!m2IZG5!jY{gOQsE3}b6eRZ$%-3rX(!VKdWBjR3#@PS_hFz&8&Ado26JS6>H` zE7@qWK6qI<6NkKr932KA<5@8#QC6-Ek$2p^xd8Zkv8FCJGEb_+d#8^MRR67PI&b*y zH~H`8>O0=F>P>1`x%5Q)t@l?xa?>}JEw&%Mx4<;UAib%4B*-EtK>jdpnQLk^UL!F4 zii2}Hgl#_z?!qx5?6|!JsmE+<>1KO@j(IaOcr)EJ)@(0OGjE0jZyJL)@;R~safNx) z^+DS#{5Cn&dd3)0bSPGPK$fuGy6VR z&HYIqdU2MDKf{!6r0?qvWf{bW)$34j|5UyZ-WY~+DMs$jNzUZMQCf0!zQ}&<`gQJ{ ztMfPO)p-kFeCCu?jqs17!fb!LAmYnUcY%|)_Gf19{Cje9_to%C&fS~$0V5)?<@wE~ z>Nlk3ceag|d>g;rbQ|NH14m|38cXZ^etB!&I^y zuBzVLPL<_nM*O(hzF#PgRga-#;zB>p{9!s}*Tj+o=)=2vfV^@ZmFJv0`j{gBH)fK} zIx-a8zm$v(&dY=IyUdFj*Uv|qxv9H5H#esoyUDq!x|eaizQOhJ;|3scv+;shos(;69{W?V3={++AT9KUXOX0Qar3=2@ zZFb_i64naaZr^;ndv3bjVzqm#*=RESG0QRyfAkKXM-S5d-?+mv&+NX=?32vs#?eZ^ zJKK&XXQRCHwynDG&g1gmCG}mFS9Il_WB4e?G}3(!yDP^#kL9CH@Xr4CY>dPXEZZQ7 z1%G!B-b@$ShpXPJU&Hx&25*GPdXwj!AOBFbb>*F}1$jB%>9i!kdtZOhzq`!e_3i89 z{aGg($PWK&4lmF6=_Uv3-p%|i5xGpx!ZThg*?r+7ck@}2SZ96ajA-?1wH3Y2Tdn3< zpYxUlJK*{tr;xp`)rwJUNxV+5VPGw;lG?y34scJ8jf(Yac6uItFR+(6~$*~(AK zSAOoMD?d_|dpOdqQ~Bi^0Bdhs`8VKkz@|$!UHPYXQF)5Wz2kSyS}f-JZNJZ29k`V) zl&-!ppPvckZxzaaIF!G4DF5Y9{v)CMT|?<9q4Zs$^n0Oii$nQ;4W*w9m3c6f|CdmH zYbbwFDBTn)GcJ^VB~<2=Q2x82{KG@}%R>1-3FWT}L#?{-rjo}ItTr?(2_FAe1n z4CNmXs;3~7KO9PS5Q*`W8whIotr;1GMFqeBh z%Z&tcI2wEeA*p4eAs9E6$@3K&I`%K3U+;Ja9U=v2D02UC68mGRLmozZq6B8bU*Bmd zm;I!y+lZdAuGYW_y zJL&#^QRp}hK4TJ)`h>4^Krgv1cDnNT=N&j$gVgn$`gLnh(G2w^g9Jyt| z|HKkpics9vMlD0Q-_{AWc8G;>LOFyv5 z>a9K86vnv|_i84n^Cy@`+$MA8g4yh*r)=9a)%*zuGsOQJ1xcoKtbVB#wBnp?vwdcL zE>LF+XR)3Oy)BEi)_Vv8G$j02z@w*bwrpR>>s^E>qi|y-rq;d}-DVgRJ2c*TF-cUE z(btu{MnjdBm%2iLa#K@PqZccZWc8=uQtrM-;~xZr)Av8&`w#-#ue~qd_>x?UDOfQc zu?$fF>vYA+EnTWht~Nvo95Hn1y+3V?F8Mod49}hq-qf3sHQP8g1_K`q-e^#QXqA4= zn;U~S78=d=a$igTf;SqpzA4Ckb4KuHta7Yz;>%);6Z(_03mX5ffVu^QtVQ>wWI^%p zy*uT%?ptmZ5ZNZXq-=m!yr0>>f8j}!=WV`Euzzpqi#MOwg5Ea#`k&ph8r}I;q_czpiVE#V@4{4OmeV{2TXvSf^q+3~^W{zfJ%iMn<>7xE z+P?|xw|iifUVI<*p{0*k$erWsdOhQC_0d#))Z;*ENS#yWW}33|&4wSL zBC85@RFn%U`xx`&$z@oj>`Ji_ZD7z;2(%DOwn1-Pj@UtNg$Yq7^hsyFDWmg@IJ} zEAC-kQGSH?4bCWd5OIY)PWqR?*|^HS#h%MhHFB4zS}p(f+n?@<;ACyg_yR%Hy`v*v zfle?FrV{!c2w=onB8W_knE2;!&3hPb3H(knPGvm#k+)T*Cxh1aTE=?iUD@NS&c?^e z6vT#CHq&X@&Fnykn(gR(1-UvEt1e4%ykMs>R=O#*!VNIR3RJ}X?i`_I%mhHu?LXc7 z3O8xg`>GGArgO^5nNY^$a_t2>9qa<&ya9=grFudObF-{)DqmI?(Gg=^{bWJUFg59I zHY_eASla2J7qS1AL2XnmVWU2X%h4b;ppJ7A6{{fz&FU&|k1Tg_laD2zBH(R7tYTJO zbe8KJMSQF^xQ{jL;co}n^ZS9$)eg@@H7vrW#0_`zoaQe;ikj4pHPO-u@4BfAnbnmI zcf_L05>*HfR9+iluhFf*?ce|>9&L*|@#kXEFB^9De+vz9u0B*tt*L|hT8rY*=bh$8 zvQx!TXTp0=*3-inb9v#`0#{rb!dy396|pEQK_OMm|NZdhs?=z>wu_Op#wu3SC2_Ld zhnn)TZYC8)%H7*DpH9&^UD4NLc_;H+e;n;c%LYTanN;!r&m zPd*<9g%}gv@z!pIFbaSprgzj8FC?}R>xmr*!Yxju!*$^4H+0YqIygX|lc&uaQ^yV? zG!4`i=MVF?P!*PgJnWNd07M1bJQjV)X&K4WlR%3Byer|d9yT3o8d_BtPyH!~2|R%S zHMMRXV$I&#KCM_Ci?%zHCx90O-Krwiq+78@2T~?iC~M%U#Jp*K!k{w6xsS!$hXGN= zfOz2w96Xu9tE_c~F4e2M^jXL)d3FmidqUDyYKJ8j1z8hB=vnH;W(m{TbTOE|Q*R(d z?za5;i^P#xrXswSVU%1iX1?5;gQ%1ftZ@y)tj7nbvAE|IM(S+=S`L^ ztSeqM-?%Zha3v&9g8fwCT{-RiTj0o&E$*X zK93FSHtACV}tkgO0yb7M7Dg(R+X=$Bl`%PnFDV=A{gFxp&!u}H zDUZP%>iwY@NH7f%;8wtSj1Hs0CWqt0c?^?j)lo)w?EIlIH_|b9F6U;z(3TPE@MV(DJp-013fazV_gdnO)zf%@xVvf27~T(|!Hd6_ymIsm><#puKre#UUK z17W2Y$_slCjf!M>LS6C$(eL=)=ZM%fP)V32VE#Br(4ewys0JR;f9u7NoQcwE8 zEbA5m0(UsgCs3op1YFCX?;Mt{>Cac^4uCj%P+&9%NAfAfg_>K&wEYO1dWU*Kla9x z^k-_Esq?|o!@!?aZbe5t+A*O5ERAMBkMZW##zW z=o9cJi%)btK&d?Hd8;_nH|tWYEs+PMoTU!cdnj5$(UK^?L{%Dzy~hT+QYkrRKNLIZTp;a6&Vj`JasnE7`_>fjEnW> z4gO?L^*ER3?bKcXe|ISTt5BJ5a`nvi$A$7+Lh0Lc-*)(^ zQ2sTc^zcx6a<2Sr|M#K%yFzVE&86r1cjU^?^&bkQ=Y-M=bLkF0-_~4zKrVl_AIa4@ z+wT)f7l+bchsv)ArI&@$yM)T`olDR4KWxszbFTl-Q2uA3^e4IWY`-Wp_WtBg&DtM! zU1P&ynEYW&2Z>MMmSeST{^ITVcz+AN68+wX{|{*UvGcLQdbJ zWBxCVtu)ZT(*3JbGHfsP`W^OlXXZxl;M@Y3>%Y##gre=UF7y7O!DUhdLf}I_>(|P~ zQIgJI6pu>%#LYrZk!+eTWn_ZO4c%;*lt(FK6twge5>7D_8`C)BiK6OcwmW@lLyww) zqsk}smVtnXweK4i8+Dm0DN;vqU~E!763nx-fFKR`^t#i&2AaJuAbWoRjhE*dFU{;f z)jKY$ZQ)%ic%R?9 zVQkI7UzAVyWu89;?mmgL0=u1729_bQKF%m=`i?UCh-QYBv)mh4Ajp6@q4gmrG)}^x zsdjbmM30H5&L^JH%7!O{h8k|RElAx!G?jB0VUBMh5-C~=f`Js2k{Xr@ZoH2x0+@U0 zT%npbyC3PO=QN+sYEaNG|11(y8@AAhoU0z9!$Dh4Gh29_{!zR>eX5+#>PUIp(r@_M zV*uE)i@E+|mst{J@6QSRr~e6^Nq$8PK;pIMHWo1fiCzfn1QL4L;a%4Go(W2SE9MMy zD+PDzL~MNGc&cHPM3|OIh>8|JnMB;iCxq{%xP=OI4+VnfC`~`VXX>|((!OZgf< zO@k<*`zX#b-yKuDE}qnBi{vY_EYFS*TNld5mBdsSTeZTq#}XOQf*8gX#y~t(Y)-6? z=-0%vFkc;ru%FmQnQ{tU&1YyViMNfLhZU8LjLOkNRC@JP8xyfqH-(dgkYc98lqh@Q z8KydEe;QtLZaj);dMO{spLViHMs2D`EPb`1A@t=4;WAPL_#*mIwGRD=M@L02=t@6I zDtt^eEkn zz2HJ7%4yzAG%!WA?35M8la;Zi$)y0Wr&JVPZ zsv36lTlmnzxhE&ji1yoCt2|4n4WK{B5o(1i-!n7Zl*ev@^Km;Qfb&K}SI3eK#olyC zng)x>qp~h~dZb~m7?(4|1~!y5psgK;fpodIY!{7xm6ynsZFtH0QT^JObGI}~(T83F zTHq?EG?d-|2NDCT+PlbX?K>?tcq;FqGj5gxSD)hl?W_09KBD)YR)y$4ztW7FucXEt z4;X5BKu5zOm6>77G%WT{qmUV&cU$oNjr{6HkY??V#FC$6d5AXW;UVVxakdQK6SKvB zFOnGU?_^DIm#zlqIGQf5WUKTHQK)+U$TU+5o@mm&cZLS28CzwrC5w>$hm-_YC5M0p zb;%hfmFl5|3`SOgk-;ZO8n49CrlgxJ^l=~W^h$vd1gi}c73srN?=PBQNuSa$hSIN) z&NGKVewdxlG57Zf-FnJXVN~(d&^rT1Lt6=;YKSM@Re;Lzr*d#5#eZ@t=jUMkZJJZu zx%hNj-#X>p?^wQcz4E5=jrPYIy>5RtU4G_wEPutnDxdED@(<}kQ!9+5r~mM<9%2$! zzBBMkcYl!g9INLNQd7peo-(k^k^a|AVtzUT_%R=6*!Z=_xT*6Hx<1yP&xBHMU-`bL zCcs*2A@(Cnw57C*H&e8VwRx|8Ina$1@>b^gfKQP>&E_|F+_=3IVb`BbMj#)oJKwpx z=jd$q1_B8+;bTW^bGQ~)bCHyug{GNEN_|X%NJ<$)txheg%EwWnb@ufxZpMt&y^k?} z-5K1}+hc3lyNp%qP;Wl(D2k-)ZEccpK6h0ExTecd2s@U1#R{{AypDux)s!7-uIv*9 zj+j7Em09>_<3$}yVeDJPi;5-R(p?)=Xd*KW)QjZjgv@W?=qMCi$ZP}y`O~l-b7RzH z(q@eHCczj_Az_Bc7ELx%)QwFHZ;x%w@SeIv21V{e-ik*<*puzvN4!(!e*D6C_c4o# zgxC%^B$CNtE_#B^hd3fm^ER}i{jaEgX*ITiR)3^c@9k|`JvM0dzFwhL6G5wwQKsKQ zG^~NN=(N0rp&@!~**;Foi=;Tl36R*(hEVqj2(<}-Wg|D_$#nkyk$$b2XGQL#frQTi%xj?<1l2yJg?Yiwb`hUG`7{^!M>UGjHe)0j#Jpm;ZV; zU%Z1;GpXZ?{GT8RWcR64;}z#6U;eLM%a^8(kNBM?uh9UUX;D29)w498Pk&1`FrM>7 zf2Vz62``dR#tDSa%yvp-xS)5bH)0zA&$??}BNo=_xxw?jJgbz^+>A`R-W75y4=*fE z{1E@Ea?JxGYcLNDM6vF2&zsKRlsvqkIKP>&AAT-s^{*uc3+@gc@7hA0F-yzw#soci zh%%YqdC&hd$3Zfit7ou}Ot%a>*vq|ZDWbuA^<|hq8Tu}P8nGdLK`-ye$EtQVqC33a zE8$8*icji7QLMhoeI{QchBqx6<)(J+B8HorOy5ZC&^U05aeavF41MUWj_2;y(KsWP zgnXm*!NzR5??H`V`z(AQu!^64ksbEz&2!`2&AVJMU=YtyMv`tUyS}QOZHmgYxJHCz zk!5D#DTuqV1q610A<}LWXXqvcPN$LXUv29nKvHqTVU$7` zM3lS`40lt9WN=p;Qi>19Fy^SIv&~V(0qz6mi9;M-_Vj;E(L6_?UZazGO%hHJ=!-M= zc#A&E&AXs<2h>+nU9vjBYxvLh;#S_x74RFeso_kn>35geMZP!aulxSelSe?8`pxe$ zz1;dI$4>Ya)P|#KhLafTYOY|@1t>cPd@5a}`|`F%Cgr}oHr_NWlh!gX&e6AGch!th z)Gq6?Zp?ei`(S4}#`LgEjCc>hlqV(^bmW%|QPAhOOlGjoyPtK-b_JO?yw^VsBedy% zZoFf3KbFRO8cTem`3|749m53IPfDmQq=cFB5>*$n)eE z*!%`BmO4j!svjo~F;z`l*0rjg@>M;{id9v8#o`wlyjkwew3!VV1>G`II5K2UQENfP z-cO|`U=ggx;m5>NXN-_uoI0;wTlIV9`6Vt2=~^RM4D)`bnw#cED1zNLb#}cvX~tBe zH>qD&y6GWR*0E(jn^VF8dp678N$jE~{hi4*`aw|3A2Tz}_`CEcxKHG3aPH+}-1I5b zv5ftZF;?mTY%Zm`c_we;$$i-FSnG_th`)smqj4GOQGJC_ zF8OMm+4V{;=K`Ga0`FWVU#6^?EE4@YG`2Stb35a>Nyh&LdZ$Sd_stQAhOr~M?0UaAIyW?Vmy%VV@Ze%xSC=Y24=>$Y!t2Z7B7>5fUc77pGxakzn}Rr z*6%ZTLHhE8Nq_bEd!62a%hkdh{!l!e>~z!l_dOxT=8e8X#6vaAZ7d z?-(CX&4{pQx-7^u`?b!*zCrf^-2!Aaxvj?80!m&tyMV%OQ_}!`3f)wbtH*&&Rs3My zi=K(fe-xiut4{h++FnM?>d5)o%|Uy~?xChuetCC=m>OrqG9 zVR)5vnW)4E`rc8nPNm)rCeH*voph&6qMoA>q@g22%NP!y$lwaS#Y7nNcus9!kBcGOodMAuyJlL1ppiYW!{~CfZmY zi+PS8iPEsjU$=6; z_=@XB*E-D~s44Enkp&`&r#ofo8G%q#@vG+LB^r#pNvPNr2u+>j+B>H*LVeY4uhxFk zJl2$*nOlxd%L(dW)12|4C26k|uYnY#W|RZxnas99@|K+p-}}2ou#XelIx}j)uHgk0 z3k=3=$?GQzki9Vtc+N-fbw08Jm-W+=^@xyjW&c^> zwJib8c!MA?+S!FjN*56kFmwnA<15cqa^CBhHsrtu`=^B8Ne4v71zkjinQd$aSPX(& z!tMe*^0s~BYc5i-_|dXhgG%>LQCOY;RRNyx1*qbAuZw?BUls#ezM*`4U|E^de3y_= zxS&1Vo$N(ix_woy234kK!V@sZU`F;Hu89AI>1GwpG5cnBSf(%4EfY+KR{QT6YfPiyERC<_TvPq4 z{S+HDo`tcqVJ7AoCi`#V2NH=S-Gl zLFTEM0Jb1A8)fcE?-8&-1Fd5WD%PRt+Bb{uUHFZ;@)?e8beVt6Z|$ydA} z;6xB+7WYB1EIOFT4BF^I#*Q130gv(`uZRu|2ZYFThKV3}7;h2;9;ewXU{Jk^GQ2mG zx|VM0zz40j*OsuB%^Jp+ucOxR!?<>ArfC@(8X|YiBY3j>gwbq>pD4&oz)x_I$)WH@ z_(_wgJPzp)vwGHi%R(JX59Pla<|nN|#Vqo~M)}E&evY3kM}E4>Ja*+L)#6LvmY;CT z48+6m6KGPRFSu7LQ61O`Gw@MIqjXa#FFL>5&2Q3ZqtuEo!{I{NI>T~ zCbXt7aiM5@@1UX$()jaCP4Hva(D&mQU{>#0XCk}M%>i~O;^S7<%h64i`ucP;lN2fK zLNxc1SS>cQ38MK2X2^e*KZa-q{n`-SJV_L?2*7oro7d{g_37qiOtrP&4tLpi2RHUd zn=5dm6O0G=a<3o$&L#)#u)DXJSfo_VH6Moc(UDyg>?)(L4VV3)U4QV(UaLF4(2pWr z;$6#hvZYiL zE_}7Bu$y4v>`|)2W{Xl_8V?Sc$K$HOuI|h6in+1m$Fa7xo5d?U1-w&qoeH8b zt=(vDK;Ty~()T_zW8;m4A`2{zUl*ZIfSp@IS< zNc~Y^Vs|%<|4G}D&H8aABcxY!*Zae}5-pI~?iPd)7?Yf3A)RSozINz7f6WVH9 z_BPvb46sfEj1kGGu)VIL&3}`H8+!tuH~pKGc^Pf?!mBaT{rH10atHPYFG_XfOzqc@(K~1x`cGg`X|Jsb~M%_K1RTC?GONi25haOfEYx|(6 zuJ8+GybP|)%Hm3R5V=X~nZ1#@ZE+FvBm47!x2Vv}myzq+V}%|5q0Ce4$rYL|P1vhU z*xWP|c8RE1PTI5U&)j90Yrx65Z?WbuBeCWVLKBNutg!0~$I)(f4FyMTn7c1rr*;7+ zVha~AQ_gSN;uW8<`R=sbkcH}lB>KJ4k8bv?t5}Axy}dEIO8?n@Sh{G>+&U4JL*baT zXI;ztlMcg#(6AT5Vf$ci83`Pbb-LpIRM}ldys0hZ_a_k-eU+DDtz@2I(wb>fIE~LL z1fbJ9>?XPO@-yDMDYew$7S7JyKm4!-p%%+LZo=l2%g$!8 zWDwW}KUa=Lr-J#+wP(m-_M05q8{;_M6_Wtwo>&Cp6^kuy&*O3~3ryo_fU_?5g9FTY zZs9xw#}9RHnm~CMgE0symxqV7nM4%oek;MV-r!501LM&T68mr>49{9KTkl%DymhMZDRXa1zdjjbv)pibfjh)fzE7ixv)8mR|Ap64TENpGEM-B3p(08X< zYnPpgOn3ZQbjA{kwMa~<8+|e690sic)WEi3N%Q=MC+mWF`sR=(vokx$srN%4miBqR9jvs!h@a0tP2@HtIdT?X1^gGJ*_lxr>yX6 zSgrD~8+Rjp(HBftzlB|jL$4q~v4JO-MXH_Obodtmoq(>?CI1;8IIc9&UE{gecGEv0 z|Ng%Pk2$Yy93InP3f~?cAF+D>7r+B>_JA%hC)GLp5PHOg9yb~Ecn|bAKKkOsv+~$6 z-eSi?i@HLiS{tp&w}l)j*zTr*(FlHVRtx<2i}2%h=n&{J>GvB!54*0xk7|n_H+A^q z^7Eg?k0Qo%Uw|GX%xg;zv-qJKzT@dLiVbCg%um9IeX2wn?0Ao=KdF7=sSArAC)t?U zHoxfVaDtS(W}I6s%0+9UgvA;9qHIJZI7mjMf~#inb6mW0h8zi-t)63 zKAwEFrFHy_y5!f?#ro`Qcpx67>BQkNtY+)fxsAD3T#iY%*-Ce01-cclxr&U{Icd_i zB-F)Ac23S~X^lHB6^TW3H67{{TXANozrlRR-OAMa-6yoX;luDMN4ink=)SrEj9IBo^b}fIr6KfDB~i zGeVN%adm823D;Etv$r(9VzuwhvI6?u;g7QX*|OwVQqGP|%LbU;?U!Nk9SBezj`DS|Mi|6G?`Q`NkZ ztr{xZ*;Iy9zBG>gqTIW}2<4H)E){#9G^vDy!H@H5lC4yEW;}{%MV_&% zyq`nTrAiii$17$0VOOt1iD4UnB0iA&G@BKtQG!Gi(nohVLnZH16+2 zF({LNLr%~CZ_t}7&~mqj#Bd8?%_G_k=Yfu(eV0*jHe*I5gVw1wKVhNu=o5JkO99RV498gJk% zmhFm1DV@PAB1P5vM6!PbabSAQ}XtsFa106_AbwB0{#c` z_CI$*-aZQElAns4ye)G}JdN@$JT+#I6rz)rFX98MBI7Q0TCM|E@L0w-J-X(SUq%-> zSLr=2rTE`*F+$72hQ67-k+&MU54^bi5%+Fv?=z9m3QRJ zy+eFpb!1$BKDnrT5~}$;Hx;AI{08{IQ*v1|^vU?#I4wHB8^^tbH^y#|ij`&_IeqE{ zTbqCYRVA6%OnKW!V;9MduL~BTMI0hYh?8>%#juArEzv4SuGTFIYESpzI8R_VZN;zU zMK{{kuneaQsR8n8ZZ0MghFPQKp$x+))bE~zXT4C9gb~b3ZX04 zM=p3j>;LpAQ6UmN+<`xh=rC8SnU99;Qq|>Aw&$I?Hk)VhC#ETD&HEo({t+YoNzQ8Z z|2q4`%%?;4iOhnH*e7m%YSZ?Kqp9V)+9&ocWfdpkA56Z5uc4P%(^BJ#-GSpu5>Bcc zE?Gs%puC6rd!Xf>|N#1lPL}Gg7XJm zIW;3vuA~MbPJ!lisdmKJVACml>{Se zykvAua&_i$yMAP6ki=FTkB%)(^phM}Au^uoRaS@FQfhoT!Pbxo+vBOTD7b^h%7_`R z(bA_<2oC6vH>(W#p<}~m^Po(Hk`BvD$O}fuhnraoxOe1yHMMO#Jr*%wZam3_L}~mC zwcB^UCLuW-=6ej&Go1IEahe7XlLn-|rEbR%v%e>Y!}cM%LI_Vb;(vHM!%Ah&NWLuc zTdgh2Vk66G9iNZHpi}7`@PV+v;GJaT)jag_Oak7IXU8hcoZSp}TkfrSmOfiKql-Xc z;R$~q0)MZq^oKEe;#);&GMcd;VID$xIteu= zp86$h;6fnwc|6rEo;ngN{-Q3qifLVIH0GNPPk4@9T)L9qEm#ik&g~Y+xID#_^JGgM zte#HV#vN=>>|%723#C^hZ(_iw4x-S2!*>TYurPSDLb{Uuu#f%FxoU6vN7YySP-aBK zbbxh;T?FfqRh4UX;+2{Ev*W+$YCh7|M|5OqVh-!jG)M3c-zu5dbM+1#V4^#K_Y?u8OFkc8PS&99YMd6GkNOQ8f%(VT7X@1cFd{Ky!yxS zh2hIhlgc?lBq7*Mof6?lAgg9S?$Rt*hyad*0cjdG*txlP^NXg@(z=Q-VwbW_2Lw+d zVH)gNlk_-|KFs3U>-}0oKnUSybYvv)W_FxYjA)G;6(lI-GS+%6U2oC-Uo*DxKeUuW zVa@V&`5%E}`TFo#Z2EO+LCmRroB!K2v|n50Cr@uB`&*Y!K<9mk0-?Er)?_YI`?{&nWG|#8)=(Vgi!o6S9^Jqb$*RW)-IkDsdUE+b}vLMsQFSaDM{_R27y5y4l{I+EV(@e`e zwhI@}&TbcWc&CzX}a9ZjQd;1`+#KrGm2X~omKufpfKLAE7^^E^r($j%9#*hWD|I=W7%Wg4L3S6nb zf^|WA#)={LIF1zdlTm)hHp5>MQ5dZu;bm8CQpXUZO{FnLX zc3|zl;GZu-=l<*b^Jliv?~Q*RDel3h^E&+Vw}0CN|2z?=oA1Uy@wY*t#_;bJH2M&#h@F*`&LljecVOo!li8xax@Pye2 zqu6ILq}Wq1l(VPk4V|TyW<}48I9F-&JDwV?oyCS;zQixdF>vT=>Gh&-rBOfT90i@L za{j203=M1>Em#QCA`M6WNL?Rvf1d&zB3mRfU2NPon?VR7&%JuKlT>m=q{p)N% zoQOV`?VEoJ^mnSuc{+A7QinyNwFGmNke<0!_5tLci%V&_W-)Tl%jWhiWA|o%GCI06 zv8Q;0)23yxgg&I$l;G^yAK9L&i!N?L+a4=D%PAnwN4_D+4Li#MI9z37GlMxN2Gf>b3U&Y5(!%p;0l8Ye*LQK_EM9uqQPY_M`6@>VsNjQ|Wm zVH5XIP>@Q!-cM+{PGWEaIWB=PM(mB`9LCA07%HR6T!~^7{fgDQn04f3?joRAhMlet zs6TVFZQsf>S^gTtJh6u@i;aFNdSJDtiHMTe8+YUwG{JPBTdQLg^I{e6$UL8X^@#KV zo7F`-V$P8(-RS2HyZX{UPM_*rEdrQ_Azs<{k&%n@hv!`KqZmQ{N)x}3SU%`+A%|6! zbnGNNpDFHeo5?Z<>fshy;$2 z!Kt0=IyJB4HITqT&eAidAY)c;8AgCL!oo4OQr7ENSCcLL{LU!kqp|Oq?e=#a>eMHtxT)gCyH)t~= zXn-iI*!w1aZido%eirPWX}sVhC9I;C>K5H>!h0I?gwUAV821Zy!y}8&usTdQ7lSpu z-IT(@DZ1`AZpACH=m=+kSxrkz4Msj5bKlvAuu3$ptWGg_NU-_6fO|>Um1Uh z_-J1)5i+-bA!}Z7ZPc64zdBvp85_MIo;tLqq7`4;SMVv0rxWePIXgBSYj&~M+@la4 zJ!G|baHh89)YY?tiN}~@5{iB zZ;9WLZ;#)Pv15+#OtXaW z#OlU*!AqCjftpXd6H+S@BZXtmt=Q&f%0oJox zuPrPn?6qO2!QOtKifdi>{_$&oeLB6R~k2A zg03+M0`}-cy>TSVheF~A&iMmzw_p2|E}_t7X}bSS2axgg9Gijpv)z00>ENx`-K1>S zT22FNv+`g({ph_^A5Rht9_KSfS{$n*b|dH_haqapMk(Rl&eUmxY0n}-LL*P#rEcNU zSHFdYf<{@RJ7Dxjk~={1K7J74PG;oysRfTV96&#|@YJ;&geOY$iZy*)IEeeAXQ>44 zZy$jfnZDWnRad-JH~Rfp+xy+?3W*?LXE=9Y$Z5GoCnGPNV~`=~b&)X}sSvo;EB=oO z)uiKT)tql@IbI?sCw4#U&2sDzh{0eBFaRf({6HJjAJ^+;W!UG02i5~>bCw2&$cAEc5ive7kJ*Bw>%1u93 zrXkgEc|*GY`6m;$CRM^_#F~c93%oD(=i2Yoh)588%7mWj!^V!c>~HErqL}0h%;D50 zB2*#U_640p@K?y}=CVd*QTufBdzHQ7DQ)i#bGZs`HTldnpEcWed~-$cT+R#b4~QN_ zcX+*mY|~kZJkm;&*Oj%ZX!<(yPGzK(d+!s<#!Ww}sviM71y58dci$Uqyd8FoaYe(d zZ~CQpSUX7#1OeeWC2;^L0VL_8k=MC1C2o9Fku`{$ldg1%nOXU57X2ujHh=j~lsNL2dFh4nHifOS0Q1v}8-9 zw7R{vpu33z;XS~CN!8a#?*y5t`i&~mJmyvJa%%RElA+Z(&qB+oxXQr6SlYaYXou!b z0r>*Do*5Y=47KtaII%A&=B;2j(}QLVNXc&K~z>$Cwt(Jy>*GU;fQTX1+moC>Z=X|HSf`#e$6O! zX8ej+7YA3gJ2MXJRAA1J`LN~Z*!i|%6 zr}x22Yap@TsPymWB4>iB<}Isdx=I-jk;QijVVI$K^33rZnHT^uo-#shV&Vs5h;Lx= zTC?b;ijZ_Gq3=2b@0I;l(BuT}F%aG$q&HnK_?e`nVo}+J?R^Q+lIL|qNAb0)7t;g% zb1JyAg%+t(^;wc zIyE&%e_rM!A_u3qA@U~Em=5)60h-3RR+G=4*U|fkBJq}Z)2Nk&Y8Viirf!K0g)aa? zXPf3SPn}G1uO+|U7R0j+=QNpj(cpXkjYy}VAzHtry6Bp|%*1FI=Byh~vhnLO~H3&%? zx%~#A5&A^gSl!;zDCkI2IR~LCk6ZH?L+2$h)JryLjW#j4YKRs5w8fon+ za}S%n>Xw*0$tBrHHSJY+aeuZyvCOjT%B-iUcUbHk%YCL!^J3%@Zvt8VD`EM_oY%hy zPk(ElHL-ELmA8h%7~pt=6O8;D^09GZ$;pDVx%0=`(S|(iK#qIp&;?*;T}q zyzSj0X1pdH~@YX~hh^4OTC@}MW1Z4C&llJe&g{W)X0UK{%;Y>;vWS%BY zONaN5>8NB|-KM$+$3H=aZ2L==Z_dr^ZrSDBOb;b8xEu!r)*!;+tJX>V|UQut|PxxZ(oQq?FjxqFC~BeG_ZCJmAuoyLudWd6r9$hf9mU2VDAz z5SJ#HM4n6UOc?4cmv-RNlL9X7ST5bvTHl39_k>A1B}5v}G3hBoe(#3LaFH>Wg^j9LH zU72*%vpFXH#p4@e(gXh_T!J~kq$A&vNl&_3!2DKB`dCx++cW89n)|Lyde?wSSKMr| zKk~gY>CgZ0U6}M~kT)%jJd^$$61&aL-wZR7ZVM?PIU-wgX7mre2O*RuTDkj5Op z{&ARJ5A4FPFB%5Fp1MJPeJh<>hhIOI=5NiGQO7(*eYUE*lJwz*+oRCso8D!=8^{(Ws%dhu3 z{Cnrui<z-y!wD$b*WL5)@av%~47z;3{CfV7{~5oIi9V$J zui-?pY;Yef{C@cL(bvGQ@4i-b4_@{*{QBq2!MEerd;T^*f$-~6J;JZ=4OP&UU!VH! z2Ke>6hVK0zTE&mtt>!xcgzkYPkM)~z(l-(mDTYf$6>ih)4ug}yY{JMZi z%rgA0{Q7~{{vY<<1UjlB`yWq-5F(;4plC$Uh*2Yo6A&-~moy~gH8v8303)kL0cDHY zO;7})ohE6XtvHI}j(bK&Wn2J3C2R@q2rjrID7+>js1Q(D{-1lR-qz`C%KW}_&ipwi z^m|qHZdKj7b+@Xz6+>QkV<;wJ{BQWf+uTHqwwdy}&3~c1eg(%G$RGYZE_9|v>lps< z3>ei?UjOh+ee(L8Fwv6mho>}CUQeA)$ZV3l&f%g*L|&g%CaC|&%ImFjnj^0Zz}}ql zdMnLjr@US-lPu2r7E+?+^*a|Fad}2)A3|QcFb0OWZP*`PEjgC-hcBg9$m@I? z0H?f8*bqZr-^Nf(!ua2i*CQtqqisN5$6CKW=D$#0|N5M?ue;PMuS;N5OL^V%vHIlo z46sH+UN?9BdPNZ-vq|#$UM_k>8{`cdk+-5-8F&19#%PL)ZP zygn8w(ek=O#u1m-)sz3KybeCc4EX<{ymqW#&zbq3kk`pm(5Z&};pZPEvJZTBP*|8Kljhc z>n!3A;hsAZguMP@`H_*=eQ$xhe&kkyebHCdkk^-E44NjdPb{$p5c*dVy+U3uv;lC+ z>ml#Nkk=nE-}@hv*Z(RcM%zqzee8dsyzcjukk{vQu2){Khfyu%b@2ZBQ%e?nf51zH-E*8!K1*LgfAOnIG&*CQaWBWE;8 zUe9eAEw9rdDWc?cdn6rEdHpf=gZ_DWJqP=*8`&S;{QYJ}o$=@7b%^*wxM%N!NAdb~ zmsgLBy#B|Hkk>nX1pA_MD203ol3(<|ilZW{onynbp`40(MTL-`+* z*9UNCV6)}5@?R*g?|DSX>pwcwE3Z>wR7-jN$zAoy>yxJuEm{8X=E&e`MwL#;MJb*SWmi-7c>;(oA;B>q?nq$?F%95-qQnr5(>tZ`phZ+33$!V{yuRgMM@C+6xgPTR zqR9mNqFa|jUjKYUQ|0v%$+4tAynab6q7LiH~it=Av7DB zX%{#AMHzX!wh ze$qX)R73vo(|-~2dOHuCXk*LGp%a=Uue;RJOflIqLe7p{W~{HWT( zpdb_j0y>8wCe<+F(NHvtb@bmVRZB$hij{Qp@8NmneC~8cQn$Ae#=NHU_!qkT$5XJ=lId!kznvBH zg+k%JjyPi!T>IAG+p~6c`_daml-8PK#rZx8^Op7btW6Y00I?|&klSOGr_YpBME2EY z(N&d7X$b8Gt3Mu>WVURBYYr%MMT&AZhuT2U4aNhC#HlS>&V0W;q^yV#Ba^Opw>?gc zx8FDcz0-QW@irB}a3aRq2M>F)*7!e0^yYg?di&T z5zhzjPe0*&4@SLT8}*)Re`gYu_rLK34YzD>&j<;`7;niu^xPkiW{F|B1-IQ{;b1`A^vMUlaMWME-KhzulfcU*t~_`43V4)%JY9$R928 zXHb4$dwzk)zf9zhp#0AE{GKBJVv*m6^5gCK?M421BL8H{-?H7_e;n&Z|4$M5f1HT? zRrdT(MEQFV3wj9gHZ}0WfxvbIGSdCoB=?R6<1+XaT~o4~ zuPMoznT)H_!ZVZf6r~m7PvGVfj@GqfG;4#SBH!U*9_o+7y`-i2%FhhW>WU!b4DaM`Bs8m<9K_+>=!UwN48lmNaQTN;#QQss@Ia?ToJ$JYFK-Ks^v=LInf;}!7$&HJ z&4&bZ=%DRgqKfb&fZqx5pVTb1mmiWgRt;=H`2k6|*dXjZ#s!oR?M*@tsr>t(y*V|{ z2)V=fG~?4lCgJjlRBz$b6b}?#L=^kpi`y424R`#}o3+(FOM2HA6oiTBE`aT zRpI_KEjPKMmG2TD?Q%4|gI17f8{~V%jM{;YeOi%#NtJtHeUqvHn8s~5pPvT=oAjvR z#yA_ge8(E-GWzr>>U^eV?!3Q%FS@e%Pv9#94dwB%&DS?BZN_}PYhR=DmEhOrCCB>{ zg_fvlvr{8wXs^A$w)!t@PW?*L>KC{F34Zj|fM)dXrP?O-uTgz#ALto;Et&aRZ7#5p zY}dL-4+nq3G$bV~;vwu9^bhCJD|r>Xr=?W^&5h3 zqqoeOQ-xe`U^|cbiBft$T1uVRLRjv=_PztpV}w3+lfWPFd;;k^7Jg3^NEl-vowAZAAd3Et;>tBk^K20?TN%G73>{Is;ac^tEU~MXb zc>a-HyVm6!r1t5q1P)SL3J0fDB_iVK@3WOT4L9QfuOCzz|AMOMq($E#3s*)*eM3#7&S zvSnFVD*mg;EcqKb@H#H}4Ft9ZfN-aAWb_rZ2*&pYhFjh&0R*-q=4A{2N0O0>e7(i5 z#GGa%)!ZxR`Q*6B3=FuW8&#VeSMz|dzi58xbbBkSqrYEncQ`VCF)m<4t4m9aRyWqa zFZ@QyhWdBmVtfB)QQb!W9*_WH_wRJ@#y{1+4h{Ctrx~r5{o4;?=Ku|w60F30BjaEM zu8>ZR^Bs+!vCR8c*+Q_Ws_PZ}wM6!Ffy`SBMJ0Wl(`{ zB!Gu4;1vBDCcp>e(1&3QoBjzNOk4*;axOe0SuYz&*58bz2;EE}>EYMixDD)C@bkDx zCX<2W2ULC-%GW#qmLmMSOb=VZnwcJU3~GoTdfaUEy1DpTc9ZdCL-^|WsvTdWsV$

-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@ zj>EgxYt#%mtZI*+(hdwa&?$sKTFtO=g}DHx!c^^0R0GNnb?j?2F!rvI`p5R_PtnHNksU*Tq<^WC!x`pyNSJ1 zb4>S3aSB?{G}Q(za#r>azxx#X1ybr%tTernMInmTGE)@QS3yObK)+i}>CPYL^d zW>?lxsqpW^YuccRWzBhG5?;Zp>TEE)p?v!BqXfRNUC~^P-D~;MafiUowqvN6&Ob0Y{GS>kX((Q1{N3d5T*;iS-lq`|w&R>p&>W@*9aabU<~>dz z$J=K2SAisS+@dQmo1;-EepY0dbhmtg!=)+b_Xr}CjAZnoKB$A|t8Ym_ox?77#-l8> z`C<(^pvwFl!*gP8qB`YO>M4pt_4^cVZhSvVuwflDa!iifMna*LR(kE?Wl=yg+$wfA zvbcy>uHti#?NgUiSIpGuf>`zeY@#EE>cm?Hc%x>F-K(eb2`fc7QifRv`Qf%rs|J&d&`!oZp-9X8d8z>$Ig%MF zMi@-3PI=EC48=Z2pBZ~B;q^gu2699^V$1un`sssg(MdhpBWcoNl*%NUZmkYQ%Ll?}W z4+=n>Xp&U*C+ah#Z@`Ro%+4N1C}d4UJ3y-jEcVOAXdT4D2^h)kQRcu(kQh)baR})k zBvBZ<8M_HA!1gFfY7sVd5vU`X+TH7N{EWesu+gdSalYmN&Y4G73%7jCis+*~Nwgiy z?HK(y;#u8j>A>!(6xS{K`WQ0J2V~%-861A3z_Z?r5r(0O^4MYI6{F-%L_X|4umnMn zRdgHV8#UbOJlF)@)G!*MD?=B6c33>EjiWIJuR1{ISFkrCw!8H~iLgv!!{k0UYEfa&o%2x2WBupG23n~HQ%c{ zIC~F!zM^3#&m$RW;YwJjt?tZ;l!&_`LcezZtHHirU%T} z9BjEYrC2Z{GRg~bUZ4~Nk}4Fb0u}ZHFr)441PPs*@MXx8)lvjFf*m3&&Rt#?O3cq`;s9|R0z%b>IxoUBg)4aAzS$A*%AZfN*tHz zkMa@rYyL_@==h7X&I63Ww{i@WkN9F}8hUtEmktGEpv9=5f_d7T&z9fH1>YDk9y?;M zCyzUfqIfDkeeL2V=w7qM>V=3obhYO*2p8;tDh`bp^2;?EcI0VY(B+TDyV7}TLtIx; z=!bH5TjJmQ@|NMhj(RkB+hT^;khDlKj_8)K3R!1+WMx>%bc39HL@(4e$K?7tZ$l5Z zSHt%5GfTD9=HeA;(`!ZN{Ow2lrnz(e6=;33Z@wk#PKU?gY4J5_>JsuSj;9hOXdQcK z&t)A>@7K}xdiwnX9{2nMR)4>l{yLolTUKwuWUx+I?hu=PFVSZ9w0j22Y_r;+aN$lM zAln;#xIg4JL*zPMZ6*k=@$tJ?$uqN@Jfr!U`oU zpTjNG#q1drPBwE9W0-bPOxH8JC`lCP6PoHWVLU5RiSodF9#c(TCL4^laKYN+~0)W&;fi8O1s!^xFIOEL| zm7J;l6aV{Z$E$|(!SRDrIX2TIXuA{T_Q+upQ*v0T8#22t>+zg-G&q4Bgf!v@*wFpz~rKWl_y2 z;vS@SvgCT%`4tGNB)stYpf%%MlTg98ji)bdcjyG!1i+lL3W=?X1TMyionGGH0DzFs z*_*UjMG1xTyb($y<)m5(K}c||*6Ap}&uf_1?R~jqZMwQYv)0CIjkRen$z8>Qd}|pjo(V@I26&)9*Oy^wjI9 zUV_w*=XoRlgXt|_X4CJ(1pOWG(Sc=#nt@fM=T@?S#FGiO~ zo%zP_-w`!P;UAI+pnu-qkLQ!!c)s1-m(<7dxCY6-AAIxUx`XizYy2`RKaqJa)y0XK z)F}Oca@x_NK%9or_f9j%!`)2b88+`*uEEAdB6S+%<0#yIJ!tZ&oT>EoT42 zRW(`GChOwq=AuSVTR#329zM$tD0=+Poth(ua27EZph0M?6jJD}yys+*$}eXdoG1_j zIm#R{3=uM{4i8T2#}x*p!LDB#6dzlI!A1|aVN_wFVJuMuGAopP)UB#)bw**AWn)a_ z(`eI}L{7*?$OC7{(kx_4ZHDR+CxNk=Ve)ysRe=^CbaU+eIH*Pn#0}Hz(ds99du`mr zHL#i`4flPB9zPTpzXPUHeEkRHQ0cB1@_uLhD%WO(iYM^~l4doF0CN>EnX6*C5VT8| zbRS%u>5rm$R*av=5*otczm6BGa#?YEJ@qo(!|gAWZv(0*E!0R9Z4VVa;qwxqa>H>D zLuyMVnD91L(-n0<^|tvycb5cYA~@|?-2@UR29p_Wg<1tF26zJLatTBEf7d?!u)i`@kb9zxsir*8iTLsM$xUI&}iyzOUJ#d5;;Z5VmL|gR)FOqX521qo1J}$FXK&Y!3!hV~nG-o!6ejOtTU4PbQWOtT zyA;4+Jwqv_gm;~8K)=p1v8OlmQ`naZ%N205A7uCv6o!+%FriDSJkN2p5A$!R-fZZY z(gL+a?MLPG*@aJR;IP(^M*ZIf+oxF~%lDVk$Pq=>)-zlXpfVfJcK@1=zNwHl>02Ck zmZ9SIZ7MS@G0T(hsy$rJBeY#z02MRa&`li$p)wgTthREs7xlZ z(&$~lUffwX98G9+!?+0@ED1OUU_IbU_r(?`&~s9w+fx%91*8@+Wy5w9;1sx ztI%8*uoBr$^!gx5n`<=T<|DV9g|7#)cyYh_HYJ+ACP*c6Ah2U_cEF-ktueA|!w)kX z5D_J-Y%r*EVKp1*nR#6vNB5)fsfiYcgel=XryC|vwa8erb3siWrq`rug2lRD0gPHD zJzCh!MO@}D%RNLX5DTt0J2D>MXxZKn*f~|>>g2=ft@3O{Kx^UiRUu(QEd|?f`t&L> zD6MAqd}4akK&3?6RQWs|%|VXKr|ZpXw<@1laxhxL9IOu=f3WEiy&Z`?d7pS{EU7W2 zkdv8A-0`LF_Kyg-@c2ULK+5{gpwl8PHfnZaKKQcywnQ_y2nT5G7qCST+}(V9{`&Q+ zE(~TB8adX3#30g}%Yoq$3I}gx9rV%cMnBkl5^y|_zc(DfqzCxZj-l(ZYHVOoyAYix zPZ)yn7PxU@)5!DM4Pb^SofvuMH!4sfYZ<)lFA%mk8;lsk)>7%dvo1MdmT|GmwmAbr< zP7=}J_6COYCd;nA035*&%Iyw%6yqOYbrVO}m7#$5x-*$0WPho*Z1MrQJTrdL8Y5H& zGKd>sD`Ne^zCdpxlB6}VoUuFWdP=r>A+15GwY{=a1i)~0J)^VI3UY$j2#+{LWd`hh za6xwHOEUC&o=^d%KRO5LRaeBU-%JgtoA-dcVhI2}fyM+bCyU!as+o`-MW1wH;jF_J zfT?iJ;sx#-Blb?Z`@_mUpOxrl*euZNU}`N1X>rq^0~}eS$s`DEF5X?5WE?NKd1KZR zx&?DolZL$&XZ0Asgz99cO>jF3X`!krpVZ1bM&WzAojomWSql^xp0wijw{pR%NFtHx zvY0+h;rE7CdSV0UNd>_-Pt(nqd;`xY9%A{DE5xrit;Cu;Bs0*ksE>n! zLh|KOUyrUdG;4&dkkCZh2-dUr{4bRF&sv;ZhN5{IHYE(7c3Sq8Ks=@k)R4Z=5D5Q6 zRq4t^mAcT8pKbR5pSIh_{al14wp8kn$s2Mn_VWjFC2Sis*k9Q>lw!g)sTw6u%wH0# zM6@bC&crm)4zkB6*K{)BW7`M*SC=pDqA@sqAneT#KJ1q)3#K?I9pyosHiayA;rFSEq}p1Og{ux}sT=4_-h9jd(ZFx0*Yvc8UuJ{Zowy|GA&;r)n%- zts7$j7Ol0LC?H1JpSd2N&3Q)D0+?_#B|glmn4A=OY5S7(5vL=mLtrLZaBoYfbf?>kqdG|~ZJ?%$0w5qD4x$%4oGtP1s zFs=$ix=v}5>pG>mLhcMoPefl)^Q#0+sj zOao=W`*!r5lVxh8;~&^8I{A@yVUy%t=N=ts1`@T2HrVJSG_6GnqCv4Jd_?vVD}Cre zK=&!IN~!sIE%t}7yEUB-_9DDHpWY}CUIjWR?M|puy`NQ7s?4K8QCTSqaBGy#v3Yoq zv=56|Kh@9duxY=MqQ(cnZ-@6`zxt$&wvJ;wNO2&!$?GF?oy~@K6dLl!B&b0Jt4Ean zwmcCIsN(p?k1RtA_U_pRIrbPGH*1aVDseJG%3angNY~q3l(-Gn!=8UzwvLHg-!4> zBy?8V1P{)9ewG2hMj^CRZ3QKDpuV6SPFfQE-F}TBQor4NcnQmpUPUu=fC4lpUqd=3 zJL81O9aVT#Vqc)Q0CTC0Y7HY@K|R4NpzR+#L8a?JE@5p6%p;5)_<_k7%G_dgq3TT- z9Yv0&VeFW#P5Agvqg_`954h^ucp>t@^y(-&&iXVsd;0|fNpqGp2_b-LS&szXkm8QG zP!OB0*j=RBc(d;P^vCX(DMoV;Wrj_;8^X$DJEue2QDcb}g6n(~VGzfN@aOPw9Py|utzwb93ODhTYMI7=3OIHy8k(Br0$;qQ`p zQA|-Ii{K`6eO18G019u}@Uvm5>gEt;eU433Kq5)bOS*N|m1{qTG8kt;0C_!|UlZH9 z+{`Rn*Qu!#kw-7v?%YJgvKkXvycTuvSi7l)!9ym!i*1_y#S~+sD z_oN7e_eP@Q)D1}&MrvNkM2LjP=<&N%`xKu=k_np$U4PWrtq~m0p;+qIhKRbKE)6B5 zo`nh{g(qtRs!XA)bmeYL`_ALi<2rT?t!xw*n}bvh$sd&!*c+*ip|PgtcACXdNf|0> zAcAAekLc3Pf@wN>ehU}^dx&~infpv#Q8+h(8CBnD0Nj#b3yc`4VClIOMFov9r_CZd zvKR%aCLxzZxueTSic8q%p!kGwv{O?$wC@gQV>z29k$0F?tRcnD9eEx^qs|$0KzKaq)}5CT-Y)o@tOvC{`UlY4&z!yJ{`gbr0R!?9WjzgNbO&< z$?o}D>z%M;_EWLu9-J@;4Jc5H1Js^6R=>l4=X}8Y~!) zy!vX%QxXc1zT?Tkl*?HouDaJ{frG@S>nlZg4bKb&rE)U+(F(9uti2iqDxojeTakfW zp-vVYoS^yjip&x;PZUrsJEs3?t5#Y4W^kmfk&L!EFrhA}&Z zcQLVKXck4dts~0{rQgL`_~m!^w^x^U#WgB+kQ2@+cv107__!JlL+%GGo|!q>X{h^> zw#Pw}NS%jVC+n3mK+ev~BkYTxK+CNRpyyoc`BQyX7tXWX)wxYc+#(Dkok_qpUN;57 zIJ%#L|E!Rm9_B2Nzz}{0!{_xPtIqqdSC+iWJ<7pNAZy;^Gw9ZQwX|VG|y`(yNz3fVOgex1or{^q`z0kgk>bC_ zh9TndW!d}b!~RKfhT=FuBq@iH2!i<}SLh*Jqr*ZZm5jxhheFzXaP45N86pKpVnlKu z$UE5oDPCKa@gKe*XBzZ$z9e#zfpN@Eq)ViNOQi8hJ8sNRUDR~MIkndFfP!~H#^oJP zc;r-!<1!P2`G)Thu)d{pcdoT5s;`6Yxw$hsO6CD4QyILBy%BkH#1O)BXYSRv zjTeT~EMo;EAr1Dsd2gB~_NIZ1ho#CXy@a~ai+0IqC7+Vhn?5n!-g2$0+&Er)}) zKdWuJ^9G1Y2&dvDv=5-2Ddhm4`#|`C;UhoHF%3S|3=$oukVD$--EUq!Nw48j3-D3o ziSQgjBC{!68L*K0uO2cv6@NlO>9Y7m_Qk2{#O}{*WB&+r!R(Ol49`q`xhkUZYsY@3 zgoG&YKXldxD$ey4a25@qYy~D4&D(iN0tiNbc7l*${aRw^k%I!K>bM_cSltx5Ns$4m z4i|(WV1&Qnk7$FuFEP-RO6vISJz7hAqu&=Uox-{hX`z~j6g@~^Q}qxfo~9pL#zWSN z7swFmc^=!md$(V&HzeOlKZo!BKH5x3GExkchn!)v8eL54NI!a9GKd>pU|z)6FnJ)T z8ydA|XKfEy0*pyDg2l%L>N0u)K~7@kx`@h=38D}dSRGDik_jUs0qlAY@Lzso!&iA> zvR%VoaI@EM>7=7ut74-h5>IkUEa=u!vhc_@l;km~FUf+yn^`mSmr&icIs_c})F`hdowO^L2 z^%fuj#Z@@Ti#3Ze0L;i7q7FJPSP|vj>w5@_K7N>Ow%DTYiM{~>2M+1)B^_3&-iNnT zp(e<)i_6Q~+spsD8~(pEdBvK|roJAR;+AaeLG4qYcKfN>J@fThNMeW>v+QPRNgO^B zj~KK|(Evy8zpmz(n#Cwf&49z77CzT8GuTQx>0w?wmNiLzh>n$cW;PCETcOAOd@Yp! zyGe#|d2TznZdR+g_Q5%Fq2+`uCb4Q{q_YVN8{AUWDWF^uAZ{vGl_wK!P;+n9jEk1h z)*E^GZKjX)Uqp7F_MAaZD{s8pMO)!ywGeG|&l$*%RvUEX&`jm6$-izszKEYD zpH%tDVS7%NF$GpK-D0-jwIYIlg^HcCja!LkB;XIf^eByhLCKZ(M|dyEofH#>qi1j+ z+|ZZl4{md|6f`uW8vOxJ=uTEuj%0ZT?jtAX8@aX6F0>KDr}bsI!J^}*Xq8r6 zk~n~A-y714zbU!b!dp0|w85Bc%gudwi$n6Hk{wBKT?g+VZ2D!b+SkKvb&@gCO#qQI z=QhK1tK~$hdBTpZn{}%Q|0*cdR9MJ|!Z~7)D5ZJ20A*FQ5Sa~8nra*-ZBJ}S;?pc0 zRTt5$hQa&UVoMp){Yrpaja4EU2%l?MT$oFUR~=bE7ftnrtEOKMRd5`E!fiZ6MQCY4 zR}k7`!UIItXgz9AsJ}9KYQY?!y;!Y;c8Ou$p$wt2eQn( z=o^ftq;iZ?rrymt4R%YIgF}X?$>k% zCFZL>Pf6JDK+N+PgWm=)6pEP_X#Q)qdq(nmy+StjO<^nn7#y0EN#TWI;e4qB!Vt#3 zkS+MTt!e^Y3bwU-w3%CzSogytH=6!LMBy>W?xG%TzdF0{bX6o`&|$yxvNIF1yx*fc z6;UM~K+M^xYoD_lvri!yCxJ@(kbfY4L2x7;lnFHCgm2O(VB@rN6Q}VTZLXMf^Y#+F z@!a$fcDnJ$wgWut5HS2`NfSNVfWHm03-9R4Q*C@;)d%vYfdS2^T2U*$lt|#hqc*d8 zqBDfm^q?G%tP0ScM?OuL;QztX4BIu3HRUvS*=D|mBxZgDX3kY$M7oLC*A451Z^WYp z-cz5r04MgawwiceSAYyQb%*J8j9$7Wi!gj^X7Xs5SQ;0QUix2@*G>7Ux1>F3J4lpW zNX9G)$F_rwJt*LiKFB8E2vvEFr{7_a!(xPhT6%Ve%lT}HXrsys;i2JbbPQ$cH^(?& z6=1)aleUBI3lL}`n>|o#ACyQy2O94(0)4^zH-!lzs{vS`Rscm%LxRGlA>2)&Iaw6> z!0_gaALIF+Dl7P!M?<3bNN&-Y^!M5z*FCG;DK3_}vN5GX6##cSP!4uwCPL%>qOz4M zaF$@T9uju2_I4O{G@J28=VQ%>;zp))CK^A*vFwUnsHd3>Ur1Pj@)RW_NrF zB9!fsIp)yN2BzNGs~o*5NCRGqf{Tt=*a7e5@U9Mn^=c*V)1;x}zDNc1VP=HtAp$&% zO8Ja31#*QXY^beHo);ttOWBuK1I^JbT`XT_n-$>x5#gmW3+YlBf1Yv1ObcY91?pqdc{(A#&fJOLI^ zjF`SERK!QE=V8Kbr#awO>@SEz@VUATI9SAcU#DMG8FABOWH9^g8RUSV?-G3RWK zj@R8Y)jGaS>2ExC0^1#(a2Vu{Bk+&%%kn!gDnd2)i|K#-a&=X9|Jg|!_8xtNeMVVl zf__}g8eNXTn5DP4)LHx|_OKakUbC~$c&+-2%p*Cn=5r&_Q+2fH^DeRu5mtmR z4DO-IPwZVtegdZ>FVTc(x*dNqhcf{B=4Fx8@lQ_u;vR{qrL9z>0%? z@fnu`W|Moit@=s)J3jZ0gOZ{3agdja4G&#d%PH#vtPvUmQe&Bv>{G`HZ(!+#$sLA> zZSPl?o6YD|$7x<#h$EfjWIUTn@OQn#bNMc8kJ6xH@M$aAk z5O7UL+ug6z9{?v-YCP4~8=P?4lb@YMc0cQVUeXWmc$TO(_80a?^nSaND*N+fzY~j? z*ha=IMc6MtO(zaHAgD@KrNRetnD;&qB}+QhXsfyeGonFLy&L@BcdyFu9uu|r3y|)k zaY>ZEC%3?Mdh?)kxPD9kqlHnFyE8p zRHR^u-W{ZaK@tPO|5bYy zO`)C6*{bG}oCd?OSJ|taHc5$n$||Zj#tS0C!7p^c<{!L8D^wvU>$thbmATrc(qczW&TZs20h@5PMiO0+LiccSPzjE;F5c zog(i{1ZqxpIgp`o!&^kwX!ulF;x#y4S$78WOd_F*w1e|&_@B1a|4@j^MvB3hca_n6 z4-KSe3eenK#Twq&V0e`?u^aVUZ$OdDN191$2zm%%_KOFwAla$uT(fz=psq~mcbH%# z%XQh9y2@&Q@FA9_Z7Z z?Z|Zh$1k6T@2>v+Z{*~?l<%q63D3gmyKG9~3!=qnm^0x75C;-6x~R|Dj7v6+-J&(k z+_0M*QL*dRFEw|ZLx~@=kze(Z^OzcFT*a=1H37OaF^QkFHTbZfn+VGM z%Kx-OU-WfGF8)MqjMVo-p&v{Fq(q(&Q_TKBEkqQ3Olk0Us(T2B>icx`tz5Irp}U_F z!_#cAV7F@K)X=oww!cjaj3(@@eTzrejZSD(CpfTMAwf5YP5fCTnw%g`QEB9VwS6J! zQvbIb7s8^|T}L9- z{(8FP4ZAz(g8P3p3Bn`Q)A|Swr$5wQsNM-#2b?5@FU_|FBe}6rhgaN=H?uWT3+lGX zQKSjGY}D+@qv4V`O*Ff;x{b!iRSl6cL{td$GO6k@xt{d0Gwg1@1XAHBafSUTfEs|s zvUKxB@##)R9JxI;;h9W!gGhj?+TY~hE6Vf$%k?zfRIO`Dyc$I@irUyxqW=^IiC}-l zuL&>5JoNP8At1mlLVQ`ySEGs6j5W_DUV!%f;`Gd`k;H?ZzUZyw$WUk^-{;Mz)#}@R z4co^ZcU&5@H7!9pyva|L4Y~G_Hvn?C=J~aF!kivNN69?j++ZqtOpW%w-fj&%F*dK( z@+Kn9maCtM!1Dm5dj_wFsi56%xD~wFweF6zk0ehQrPw9KfoxC0d9SWhQX4EG$u+Le zFOg>bCyd8tTutbsl^I=FpfjVkK` zrn8HSm0io6vJy%IqsK?ma(FG5YXO$Z{GVOMmE7Z&Ii@rz_+)QR_0p>?z@|l-bCUW= zm761FUsRwR&tWoFI=v}-eDy)EXDP=|N7kl^|EOKrrku%J&_1u68ayx)Ra-9gn8bjm z3p$Xd@OVX#B@i+a-o$RvrIk3#*u)i+f=z7v9H$xy%q4IQV-dlDy42&>~n2w1=Vwp}fWLeX3%*BjEVPG5tbdSPMo=ZLgr(Li(r6S8ifj>COA zrW;~KPuZv8J|aZpj2T9**RR~Gy*uj9p4v1{3nqXlV^X^zXm&qHVI5C4nU)mA1;$~& zDbo|d(I_8HYw=zTF^}&Xmx$=$aToo3`&{zeW@X1JaSeV?CRWF)UX>&snA_vh-8$3}pSwEg=sGzoi(KbuZaPYC;!T zHH%EOxGJ3vx?89w7f*?J*-g|G#tG#=v6sA~(vIUNmL*&*W1I2|XUWJI{Xn&Ri|0td zqsSyhOKskqgsx6E@Mc;k~+8l#NA8SrS*E~**tSWP>A1pX$t0gjIsE*v@AqOHqWSL?= zVm=*{Mx==Y;Sw*GDqfP|_hO}PEkGy;^gkXk?Qk@iAe(($8Mq|>XP;a_tgfBq)A?wP z+4CjzLnXmP0OTZU=kAyQ_$tzgtxhfCBrPWGAg1DvLAtG#EVFZA^CNP8fyx3^vm(vgu-x$RC|JPYrvaNC&bNv+-7M`n(*|C;*nBVr|bf|0)}B;&d?Y zD(O$o^Qurd4Y@h^TO~&I)1e}539X^5nD&532ZM67JjA3VaYCf9NJz@Vq6LnF?AIt8 zrE`5|ZRv|~O_|_=Vz!h07xp82eON267$qGsf)Gxok0a>BAV}LXqAyafk?}&_&iSRB zTdzmxQaWPsW3Gwn9o= z$py|GQ`Jpq-&svU!3I)kSNOd0PQY(xKq}(wX;!VZe}_ycXs}L6W1=L&yn4&Y3;S=# zh8znK+1)T0ee~+R;iLcmk5Ls#71O56-i`_h>HQG~=ovVjJBiPAVTj;G!$eRs$px+K z1uvAaIDGW*rFCU%zAk)$loc~{k|Wjw;sOjCwU3f$e=jJ#P@8dow+FC2{O#rgT9RuR z5!wA_wu3&^2t^cbYw4G%F{j5E*Ft(L?mkGH^8mtHD43@aEHJ>QyXTdf7rjkYB{ku% z61OFoQ#z1!nu;ujWvReN>hj|1;vWyf5qfB|AZ}9iD}j(+pFL0KU?pq^qKxQS$Z(|_ zb$@DE7EQsXsxYQH0ip|m&p4`Z=JCxrN;aPop?<%aUOi9I zm_oELD&L@b!`u-hW`a9PD?0t6=l_C5(~m^pDNwcUD8C4zaZL>qSLjqE62#OPFJ}u7 zE}_5>m|lp)b~okuS1K8Id8{cePg<|UEAVtRa>Z4gO#f9id)(C5dIaiLlZly|L`yMV z+yFdD5A1>Buvc?WoiqD%dor0KQF!4j6nVu8=Iyl{ZUHRJCW3~?1Yl%)Y<(PXm0%xn z1NPgzVUx|u%CNh>ib$kzM4~u3jzHFhZJbPXUI`XrUUJ0k1Xe2elgvv+A%}-k^*+5F zMtzJgXhYo4ZEN8+52Kh_(WUa>?Z+zmm8swoIak#!-dw4roC8YIiwB_Zbkw7(bYW06 zmkx2_4xZ@)(r2xH@xm8v_g42pxJpW!m&M~?$toyGiA=0Y3pVcxY@oDY5k?bnZidk* zm3pvI{MfL2S~PJVvv-J!cA6wj4H_f?OY(XkB2ZVTV2%=|5zkfsJh{ALYk|$OtsO2^ z>)~pHk(fuh(cf-G2j=C5bf&YChP|ihNQasEW_7cq(XW66&xZ;wr70|8d-6Wv(%75v zulR{Yd(&sCFjm%b{D&^r1kivsDR_bDux`Go*q-bW%;f%ea6lxteL1JIkza?!g z^1-{!Z2G8)V-lz{F?hPkb6+SAb{z8tv~ZHAk{2zh+0#L54{;692D1lkA-Dc)$CSU$bjy~JyezKD86qLYu$|!QkrgF*yt!`(AqV03iM+ErH&6gZ*`CNjItY8Ks-_TOjJzm2tf|a#; ze?919U|E-=xk1|)S^Ft6f3DApG!Ih9KxiWBLS|U~EF)D-6+R}zV#kGbVKUo=As|;` zKF_V~XX3La5u^y0)nDBC*K9*FZHv(^$zf7nR}=mY(R_3_LxLxtXs4*&-RTO?pR0ex zDd<>_3VlhkkXT4+r&DVK-EjUZ+lOdoZS^8~L}$p9knu5x+3Xx(O{-$|Kr2Q#y8m;| zn$rvb{0gu4lnmG}#qgWiue2;ty76Z(yzvpVE7VR0f*TRT6>_tW@OP(DVF2&^2RU16 z8)8WpZPsBEZf>Y@gSl3SiKpN)%@KeyUt~9Y`1H_`(8WEDARQn&u*9Qe>ss|`teAB< z2M6R9!N<$=6U0^3;D+#dG+KCKoYz|T@Dr1}nJ#fO`C~+4J>6W7xGuhu69tMJA-m8y zr^u&D!X(AMPWdw(Dwz|K-`cFg?m1QSR;aKrefTd-22pdmf`Sr=OG>(+#GLN=v|NyL zEBlXFYii`qr)$E%=?3i=_2JU$%}*)=Rh2vYF|C?}Ra%iERofj`I~kB89sBLg8jQ`! zaJXrjqBtVxV<0g^hB4Cn31{rMl#=hnYzmztGoOMASa&QW%*b;4fDiz5Ic8L|K-+kN z1nE3M@s#LRv2EdSLfAPI$c>94lq-vC-fd^%zX}xKF{R=_Llo(1zNBHz}8e zP324k{V4^^@W#?A=G?7r=*Fu3D0-_G%<@hNUt-Njmxx_Bt28qOPS-FWj)Jfq1EIWM zq98{NITm#*(L1yT;Q;Xjjk`6FyTpam)oEdPsD&i&Cgfw}cmPX1kc#f_&C<`6&#yZ9 zY#FV*F$F!kOJC?}dBz2DdREYj{e8N5no^C5lN8F1RCxYr28{z+0?TcoOk}^D!PPJ* zl^9Ez5|^bZH@%6Xpb9`R#FQ&=08j`eE&!%%f+Qk^P~xbfQZ@YT_J&_mytu%_Sy?VA zy_nl^oE)`W;8UoClAI|L*VDyKi=vFSt5st!((5K2Ahb+H)-alfD7dO)k zXGI~MCIkE(v(YL4uyzJBg4Ez3I%7^8AvGu)0IL1cm`_Dn2nZ)=?SX`)N)nPle7MBs zW|C*SSC^#6FywJ(xfuyKh_|fH@0s+*!7F3*<@Z`%q_YSd)uLS)?cEA1lE5h;R$<>2 zYa}-=8r()x9i%Nx9Tq{n9;Khs#DWbRvqj8C$_Ag8I2XGKgrDCtbdFFAJ~)t=E5Omj znSegk^_Mw%w2HDiQJ^;k#1OxU7}LwMuN+~V-YypM1Xg1GByk$QN```MP# zXW}8@L(jdKz~0Is!w(~JyU;-NRbhV2Vhv)at_E@G&VEB3qoKofzSf2Qzi@AGdOgp7 zq2+;8pyqhk$JcC58?BW6upYS1grG!NhKNDKeKBsL6ipjHR&W7TIMyh*bn$;eD`X%tYZP#*r9 zkR&B1I4y_i=p0=_GtH_9Z#27rcOyTwChbzyI1#xJaqazw)oKg#qF&E49dhq)R>fX_ zpFLCEZYZV_%~zH*Iw7PI^(Ugqcry9CnLW+mU*$|6mw7qKfK3729B+=yVUBQ#>rm1b z8GsmBZ;XMP3&AVP9j6+dDii{dS2F;C5x&Id)&N9Z!+f&V)sJ*_l8EG~a)@@h-QjQm zKj&z}>M@Th(b9aMyX!XFQ$E^(1hSCWlGD&hX@Egq0`|m?*ojBx_ee#%+bL#1QbDXY zhr|H`RT{5K#oWdMWrJ@ZZG70z=LE2ES1ewCfA5ktE<~9CrxTT>Cn5f{lO_lsv;F!D zJS%hv= zQ#_y&x&UjL{dpEWBhSK>e6&1`ivnyxT@OE{fqXt6ij)zkmwsx^fpAUwvf5KtLg@xD)^2P>(tb@-aYgZFwmiX zH&#ru71?8=md(bq-M=uPMU!x}A#gXCGl8Ws{|Q$*A-bN!Sd-Sc^v_(coLKir0}Waf z-x0)ZwyXIIN3+b^#!1teb7J%=?f1l4q$l1hHXA5JujX?OImO`w*a*EYZXC@!|3bIf zRI>#1sN1jUl`>kRqT@V!yNCJ@L1e>a=gy1!=rf; z8=V8!R76nAU~!qD&ah0Kd%4{N-2*Gq5-K{LNlJ0PXTrq$MiN!R|C;q_3^*Uy)O!9! zl?$}SgJ$uttM?k=g-U)48uTa9ovP5y4DHxM;Y|s~-tghtFlVA15d>{B1lNVuWPF1{ z;MmhCn6k@u1zH@FJF^SJ-4b5dSLuVM9oq!)vx{+`g@X9wD=7oM&ZpIPh^(*n%gJAZ zkr3|ExaxR_3X$25S^xeSyp+k^=yA#|zug|TIE@3CUe0GvU{P@c1ZH6__iIBBU0@9O z&ToDR7t?%060htF(JpKXhm~Yqga5P>AOnWTf>Fn)XQHtr+{19DGb)W0Zh#Q^cyrah zLTJ_@2G-}zH82kuisG~ib(hocS~_nd%eL~uh%*2o^>wz{cMo+;UZivLAVg%sd@+mI}=CC6^Ung zD&BO_2cK#NXFp(UYo<0u297zdOBAXcP{n$p%>o1`ZWMW*sqixyYsLXm|9VA=aAsbz zuDC^8DSqxf$jK?8Yv;B^0x_(py*)v&U*XAnNH(UL$-^ZzAvy47*2MSN15M&Qq8pvu zD+gA@#8Z1!jHd3TuyE%d@9_50QwOL4^viPef?$ve-kh%=I(i_qo_#pyX%&t~eY{~| zqolkDDIhuBK+H6Ttvpz-ev~^PHRdpcyYL>4n}1=Jx^DYNu|QRJiytp6@3}Uy%Piep(@o;oQJk zHYrdCPTGfaq%;;8KfSvu3#y77JjjH6TB9MqV4>)zLx~l-ZoZ#>s0uUdv zg`_QkEt_)yD@ERtlHw`D%MMxhaBeL-v{*b;~#-d)Y46)LHr%_C-WytaFe0 zxAfJYanWE}J@YuA4l4n?EGI6ko*PqFLGY5A7(Z!C5Y#EKda z33U3y;H}N1>cfayJp>=-?x~ipDFVZVwDF-L9t7V6=cKFYNjS6qfnWtBq(==Z7?Px) z3IATg8X7l_?-u&p$KKOsJsv!x!*%zZzhv3@ulz@D!&NP8htwrbVJQ!Z+23v-y$W5` zY=$8wV?!U8L*jY^Z5kdh6E)9ze2nTxK4kw+De6 z!XG~>OiK*$__;*nmRbylh!6;Q@kVTCosaFN8|U($Lv1a{d)g5!U5ol%>`D$Z^jWo}}X zp2~bIBKrU>m(V_tyi2ZiiA7bY)Z#@ebcSh|-=0E6Rks}|DMiCN{$e_|I*?)c*JRkW zNrVysS7;xz-0h|sh$cpbAI`#gr73_Gj-*?bqsiC(c2^<;$J8r_otDy*iagT*P|?Tf zD>j%Wo}oBS9*8uoieirc63Ess%hh@d$&w);MQCR()+|zmib%J#o``TKeApdi0U`OU zKKcqTMwy&21N?&$6lQ_k{aW5<EnLIJUqpDTy4%Z`?0vm^ZVQ3{pahe%d20D($e)BEO<@G)K-W0jX@Jg>4)W$ z7g!R4*`lrLHr4~fLa2}ixX-TUt1Vgvw9|F+mF__kA{sq^olUBK;%{$@f8!DefrBw) z!)j;|rHU)G<~ye-KNUnJhZxxR86DMawXS7?faZKaBMCq8_Gmjf5tJ_Sb-N#+fy=O> z+d1@Q_jq1x$D=jqNt)Z~T#fL58-V$iB?oK!qH~bY_2Bwn_vmet*R~lhA5E$l;S)AQN zdSl%zB}WI%5}yVv`rYdlNKW%H?wLEj33YCd3Xzr3A=g`{kWlvVaZI1+m>b1)g&yr< zzfM&V?JlBG_hDOJOv1FUS1d+9KZm9QQrM5x?7W zr%zGVrSiFw7{o{MLkW+c-6fR`%5M~JJb|(N2}+KV@N;Bl^a_&o;1KzFEVwn;!QoRr z(lKg@F&7uqCeH^*LcDNMyh#n7#o{t8bQv&zjT~<{=&L8i$w&~WMJnuU2($T61H$x` zJ@?_=j=p=qq;x)nm7WW)(j(F{aW<_Fy_sc{=eyO8I_;1zms`?zmT5rpnCz+Rfmlp3 zNzsj_i}lvL*t_$1LC4vx;sUj~Kkn(cBHF!d$q2eE2lC$Hr8xU0e@q_H)Y(43LuMHp zlRMVhz-BqdGP9aYc1E@0*p`I57R(9NT(5e((~A)~s(@eCyx(I(3UNE5d!H`)+PxDs zW~Z{<=pyr4p1#LylW17)ZGYGiW?8Skm#by@C?k855q1^xl6)IyT-=kT`~Jga&@AiZ zN1OehOxfEcwf`5@y*!5qJ481=Z$wxDIVH!3yt^@|R3)s^<3Os~{9AO8^RsZafS5^v z3Hhmp53vHT%&EJnf}xy>w*7i=7Ai}T;yQrAjh6p)10l0g`QVq;3PYYr%ufw^21y?? zqo5doYo~)9O>_?KcW{`{k`z6weMMcmmE2E%@5YL7&zKcM_nl@OQlvcH=xX~Pu~{L} zlh2}rB>W-R>***>Oo9%F9Y<)tL?+*pb>FX+th&df z4Cse_95cB_n=kurpiZ+LZ9-+;VYZpjxdfWamOUm^vqd=8k5mM6(hblV4JdYxWO>GtLy@#t2^Y8NKi>);%YwHDgt5b z=@YXg&y8vdidrDnP%HT%BetwRP=3#3ep}5JOxZ%U2H}P=2PJ6-WThTTBePy`n@reF5GEiVBuKA<00d zr)mU)G!^BN=5yN|mIU7_@T9k9 z{SH2JCxG{kI)DnmOAYFDkl(jpmxq*XPo;LzL7UPQ^i&|Qh2tWPc+3StXr%P7nz%!@ zI18l5C|O;U!wI57VtjczRwzHf7Lf21*K@B?OQ;aB8dQr|@vnd~B}A<7aY+#?5wNNh zv8rRiP+%4LhE_wjj|!~Xa17ZAyNW|C?Kau0@UT{8BDw+=d2{1TxiH90xi>qgtD|1? zugz$^Mh~NydW9nwT^J_RgP?iB)rC99Y*yKvq$%`C4Cn8s(>0}=h#*yk*g2Tl1v_1V zV05^hgzm!U(RB!OOj;{Vw$0ov{anKLp+|(e9J7ZC;j9ryqY3=fL0AE;=}2B_)wKa8wF8dkdBvd$n-2#%!o@E5q`=Zfd+?1gGQ?bIfSA(}`vH#?swVbYh;9D^;Vw9}In@LX8r z$mAv(Y$z|6OB8BN2}1dId%q{j@1z!}rgVyXrQmk9+H&7At&}^uHx)jaO<_sYIvS#~ z<~fnnroSMvl%~>X=XLI8P<)sUcdL=+VEjmZPgup|8r^%MHi|MlqWQ+DZmh0K$Mf!$ z3<-b;C+E&fYW>kNu{rF$68aA>+1#<;K8UPVRq&Nki&jEc-Ly3yV*>9$y8h_)sW-+2 zZ8(falfa$Br}#t4y5+R;$?u6gsk;2Fv6H0OU9q7!31;8AIW3Ynsa>F$wR~i<8hpgi*aJuL+Q=x=t5UA%Zyk zr^{dN$|t7#j92rO(G@2P>nvBzg(le>@h%NQu36FBQk_Wlg^m=h>Uws*1x0H;od^|a z3@KnG;HY0~tSqoT%%s}A>LM4C+5LRS+#4|6g^VX-Q6^NRli56_p*+!p_LeNi#7U21 zpmPrirTO(S0nlE~pJmv2GEeP-JPsT}m5^)7YtG^;3`HfKT*eT=4V8{z9N2z`L|n$1 z)zdrsqED5G!;h#cY8a;}%(YN4h_y%xC$+*RD&dif(LaU-LPk>|i)favI`8gF z_!vI|#*XE?>ax-|`r9`W)(?zOLr20ARf=Z(Ms~4XuT&i%A|^&>yqk*(Rk2=;pYMTp z4wt{r=JVMWkji#-KImjRHk`|G4h^SSv8zd{2&6%4k$X!ouRp&aXhJp^EnF!&C*mjDR*FiH$15NemMlH3@H2l&3+# zXjA3A_xr`-m02|6G(Hg4hBhT_J zrxTD8Mgq}Y68`6AP5;a;==(frr`|8%DH5?&!Ux#CMZ>`P&s46^eTF-Mn6r**oB~x1 z<%X=MQ%(e=pcqIe8CM3Qhm&Pji#3LT0XQ%b?W3*uwhP8^KL1VVvF87Gqy)4 zm&S1Ik5%O087JkQ!$*>L0?8+?IW&`$!_V zX}sw~Zat`2@>AJZfMt9FtK^B|sBk<{H9G5g)IG!dJXvAm?nREsVTsdf{ z9wAIs_3Eg^)nV{otNfIFkxX;$&6QeD|ZA+mn3 z*DfeYIPdu3qve!1|3E@@0-!kz_Ip*{>Q&yoAS8@8Ot})kgPPiMKnddFX12t5C-ES^ zkCx~&+@PA~{IH=YLGh?_)fp>*3qIo}DDgw^`tit2ppkkuE(JF(UuM(qsY%i3p%yy< zs$KD8JqICQ33P*iR433~x3gH~VoZ`0JaLcy`~H4BpX|o-?dHCusb!Dr5|BPyDErNi z>ke|PHGcX3vG=CiZCpv#Xg^UcW?~@u@6!O;uw;oXQntJQt2@6vLASL$G-yIQW z%5x$UnIKeUSKnIIT{cM!d4?Ev?AVbNAFvKcSTrLx#4}Ng#}mCVc4vs(g|wlde4$12 z+0cQkCQPdwsyK)|c8C_N!+~KAep_4ITr%cigwoA*S5MJU3`OPa*P>kwNlJ3)q@8Pv zsZXeZ&KaKNF`P_PMrujv8gUh70OjG#D2r8hC_=7`_PDXP4Kc4TNmkfdGZ}H`B&z+w zn|i(iUxrsyB#@gnG-5d&ps$AsL<<{oaf7+Q=uM+hX2ygZFdEj}C9u7Vmg-}|!SU*y zCqP9zx`S3u$S_;$s!uqRjRmLJ5%3@L=tnr1fNR{RqJ(sBhcS_;Hd$UCqwNZ$t|lv# zA=bsS6%gp?GJ3qK{z|Ljg#NpC8|)GY7xv*wWr|b%XIHrTj?#&6cUM}{K5u?f)|?_Z zP9#&9o(WfR^swrvI(a*)Zfh*_a5&vhkR0p#oW4qj5Q`6GQ^170ZZf8TN%M}dKRFs5 zHqS6W5(rATYuZ-XP0^BGDt{y9$)}x7Ut*tSQ@|f~UhT4L=W?NY2?0j2dGbu$WzL_R0?#R7DI6^kTZ-*J7=biSedo=Lv3jUv3M?IjMy!rhB zl35rF4mu^`)b3;tEq;sZ=gUz}BY3nwrBw5rds@Rl&qjFp*?*VYHAQ*hZ$-wa(laM; zPb-ikTDx99?!#C3Bc^2CF>-kU&d_}yC>Z<^(1MkCI5t+Pk5oLA6|(CgFM}$=cKysn z^K>d$&}LN$P1_hZ=eyOAIOX&#*q=z828LMWBjxBeu8eMOC8Y2x?f4 z2DeZlGc!K|FQgJ?P{OG#5-_G5ufMyb)m{|%V(CS3Jtz;Po`j$pe05g0`~XK*+i9n* zsQ)CugF?WwY@hSDYwoQF>94yfM!c^EU!9caby8j+pld~BR^Yk@7ndIoT4@9Te*D=mi$a3`!=yXRO{X6rWUa3DebZmFxn0H z-%pTjl7&8qL$?C!#bi?$qU7;y?I4}!_aT0qrGqPn5;Q@G86wkm$R#@Vua;NM%*?CB z6?dRDZ9;u{rQ1%NprUe`ZIK#^ibwV?3k}z=>36C(Xnl%i`JHyuNN8(V^)G0twNjTc zA{2Sy@^)&QQzffX=v$6%1goGM9lqXFtGnSaa8GxOa!R$> zE0qlCI&q=9)99)hQZdtqQy4)WB8c9m8t6V1!m!P46aCB=XOy9m#0w@Dxcm{^IrHd;y)$>|D+~2OvzL z-hws@dcl6!AI&+|O!~0D?!XZs69kY|4NBaj|66#~N<>qW z042vihAZZy;nYXCij%IaSz;`zXr5CcIT=yycC{;n6BiS*k6I4V0YPyRRSMrhy};+LB(=hp=tv5E$AVy!JV3aFYuj z!$%1H7f|8FWn^W3o~&kQX5yige$-O^sMkJ3| z=IC0QPi>g4CiN9^1UL`7SGT+M0@_Jell{Z-)dwZL4NCAsXs5lN@A>V6$f>C^Q@c%B zpY#g;o|SC5Nbov)-aGjtb{6XF+=hrMl9Z{*zPY!>G}-L zkf`3@_#UN z*8ZkpLM27Xh&oa3Cv?2zQlVypA$im9-?J>sLn`w48&d(N@my=ds#i5(m7I7!*f(nd@II7x`_bY`#+Ahn77KhI@S`tL zx^05>#dlbC(Z}-#?#bQ_MRbDMbs3GJvl=_`W7NoQs}i)>?ik)?jeahNp{Dy)tiyhb)IS( zdn(6?yds7YbpiLu%-^qmOCER0ykx zcNZT(!@!g)f&TIDogg=g7#_jbMox=EPQk>f%b z^3=etAtXw^qF(YAHt>O~!E~;Yz{lM!HN$?X&`kHE_|jte(Y(u0U&^37HUzZ(si>;> z9Gpj+u6A=`uAGPyK6k@QRWlbcXYQWQzq^V#Uc5oS-#`?aIR!A-8-xI z@DW5K%V;tReIpiFwlZYc*SIU|F{G*uGxF4O(q1ST`Ck6 zh#hdWNKru#T_h5523JKD=tw$(4i#n^s;y1fV+wEvx9gaEo~z(ZpV%JlG=kgMna7&a zgo<2tcNcUgu{#$ktETx5b!#X(dO>^Du{%Z&&VWwsS5$}vFPnH6IFo~Abu}PQoJ%xx zsM$RvZ`c0I$NUouH|f^3E}sziTPMg=4n(8<_x>UmX9=9fulre-+X_9=}EN zQUniIf=eiwUyt~_hWjDY%5C2wx5*=TL#7aFdMW0BAJ%asi-@5HHZ6}n zAp1kbFvO@VLTrGGCUz7pcv2YdB&~mpGwIABvCvG`-=ZyO4+$ReJ=~s9k>)!vTw?E~zA_Wb}K5P_5BV$@gMqmjySg_XkbwO?LPwTIs|6cD0 z`$v*u|K;l46JanrR=X-Tq&8T2r0m8k5b(DipjHrkYqKuL?8*pb3{+09-!Xl7WLaPM zI(hLMXZEa2pQxZ&yNjB|uf>%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 2/4] 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 3/4] 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 4/4] 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;