From 9ad87ace0a5111b9ee0f9c366bb2ab61205abb46 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 12 Mar 2024 02:11:29 -0700 Subject: [PATCH 001/122] chore: Upgrade to TRT 10.0 --- .../converters/impl/constant_pad.cpp | 2 +- .../converters/impl/conv_deconv.cpp | 6 +-- .../converters/impl/interpolate.cpp | 50 +++++++++---------- core/conversion/converters/impl/linear.cpp | 30 ++++++----- core/runtime/TRTEngine.cpp | 47 ++++++++++++----- 5 files changed, 81 insertions(+), 54 deletions(-) diff --git a/core/conversion/converters/impl/constant_pad.cpp b/core/conversion/converters/impl/constant_pad.cpp index 4191cb1bab..20a091e730 100644 --- a/core/conversion/converters/impl/constant_pad.cpp +++ b/core/conversion/converters/impl/constant_pad.cpp @@ -55,7 +55,7 @@ auto constant_pad_registrations TORCHTRT_UNUSED = RegisterNodeConversionPatterns util::toDims(c10::IntArrayRef(stride))); TORCHTRT_CHECK(slice_layer, "Unable to create slice layer from node: " << *n); slice_layer->setName((util::node_info(n) + "_slice").c_str()); - slice_layer->setMode(nvinfer1::SliceMode::kFILL); + slice_layer->setMode(nvinfer1::SampleMode::kFILL); slice_layer->setInput(4, *value_itensor); if (ctx->input_is_dynamic) { diff --git a/core/conversion/converters/impl/conv_deconv.cpp b/core/conversion/converters/impl/conv_deconv.cpp index 66620197a9..bf0df00c64 100644 --- a/core/conversion/converters/impl/conv_deconv.cpp +++ b/core/conversion/converters/impl/conv_deconv.cpp @@ -61,7 +61,7 @@ nvinfer1::ILayer* add_bias_layer( auto* sliceLayer = ctx->net->addSlice(*input_tensor, dummy, dummy, stride); sliceLayer->setInput(1, *start); sliceLayer->setInput(2, *size); - sliceLayer->setMode(nvinfer1::SliceMode::kFILL); + sliceLayer->setMode(nvinfer1::SampleMode::kFILL); nvinfer1::ITensor* slice_output = sliceLayer->getOutput(0); nvinfer1::Dims constantDims; @@ -194,7 +194,7 @@ bool add_conv_deconv(ConversionCtx* ctx, const torch::jit::Node* n, args& args) nvinfer1::IConvolutionLayer* convLayer = ctx->net->addConvolutionNd(*in, num_output_maps, filter_dim, kernel_weights, bias.data); convLayer->setStrideNd(stride); - convLayer->setPaddingMode(nvinfer1::PaddingMode::kCAFFE_ROUND_DOWN); + convLayer->setPaddingMode(nvinfer1::PaddingMode::kEXPLICIT_ROUND_DOWN); convLayer->setPaddingNd(padding); convLayer->setPostPadding(out_padding); convLayer->setDilationNd(dilation); @@ -293,7 +293,7 @@ bool add_conv_deconv(ConversionCtx* ctx, const torch::jit::Node* n, args& args) TORCHTRT_CHECK(conv, "Unable to create convolution layer from node: " << *n); conv->setStrideNd(stride); - conv->setPaddingMode(nvinfer1::PaddingMode::kCAFFE_ROUND_DOWN); + conv->setPaddingMode(nvinfer1::PaddingMode::kEXPLICIT_ROUND_DOWN); conv->setPaddingNd(padding); conv->setPostPadding(out_padding); conv->setDilationNd(dilation); diff --git a/core/conversion/converters/impl/interpolate.cpp b/core/conversion/converters/impl/interpolate.cpp index fad2ca5121..61721e3d2c 100644 --- a/core/conversion/converters/impl/interpolate.cpp +++ b/core/conversion/converters/impl/interpolate.cpp @@ -72,7 +72,7 @@ void resize_layer_size( nvinfer1::ITensor* in, std::vector out_shape, std::vector scales, - nvinfer1::ResizeMode mode, + nvinfer1::InterpolationMode mode, bool align_corners = false) { TORCHTRT_CHECK((out_shape.size() > 0) ^ (scales.size() > 0), "only one of out_shape or scales should be defined"); auto resize_layer = ctx->net->addResize(*in); @@ -141,7 +141,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = float scale = args[2].IValue()->toDouble(); std::vector padded_scales(in_shape.size(), 1); padded_scales[padded_scales.size() - 1] = scale; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kNEAREST); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -150,7 +150,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kNEAREST); } return true; @@ -172,7 +172,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = float scale = scale_factors[0]; std::vector padded_scales(in_shape.size(), 1); padded_scales[padded_scales.size() - 1] = scale; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kNEAREST); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -181,7 +181,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kNEAREST); } return true; @@ -203,7 +203,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = std::vector padded_scales(in_shape.size(), 1); padded_scales[padded_scales.size() - 2] = scale_h; padded_scales[padded_scales.size() - 1] = scale_w; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kNEAREST); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -212,7 +212,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kNEAREST); } return true; @@ -236,7 +236,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = std::vector padded_scales(in_shape.size(), 1); padded_scales[padded_scales.size() - 2] = scale_h; padded_scales[padded_scales.size() - 1] = scale_w; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kNEAREST); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -245,7 +245,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kNEAREST); } return true; @@ -270,7 +270,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = padded_scales[padded_scales.size() - 3] = scale_d; padded_scales[padded_scales.size() - 2] = scale_h; padded_scales[padded_scales.size() - 1] = scale_w; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kNEAREST); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -279,7 +279,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kNEAREST); } return true; @@ -306,7 +306,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = padded_scales[padded_scales.size() - 3] = scale_d; padded_scales[padded_scales.size() - 2] = scale_h; padded_scales[padded_scales.size() - 1] = scale_w; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kNEAREST); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -315,7 +315,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kNEAREST); } return true; @@ -336,7 +336,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = float scale = args[3].IValue()->toDouble(); std::vector padded_scales(in_shape.size(), 1); padded_scales[padded_scales.size() - 1] = scale; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kLINEAR, align_corners); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -345,7 +345,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kLINEAR, align_corners); } return true; @@ -368,7 +368,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = float scale = scale_factors[0]; std::vector padded_scales(in_shape.size(), 1); padded_scales[padded_scales.size() - 1] = scale; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kLINEAR, align_corners); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -377,7 +377,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kLINEAR, align_corners); } return true; @@ -400,7 +400,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = std::vector padded_scales(in_shape.size(), 1); padded_scales[padded_scales.size() - 2] = scale_h; padded_scales[padded_scales.size() - 1] = scale_w; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kLINEAR, align_corners); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -410,7 +410,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kLINEAR, align_corners); } return true; @@ -435,7 +435,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = std::vector padded_scales(in_shape.size(), 1); padded_scales[padded_scales.size() - 2] = scale_h; padded_scales[padded_scales.size() - 1] = scale_w; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kLINEAR, align_corners); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -445,7 +445,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kLINEAR, align_corners); } return true; @@ -470,7 +470,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = padded_scales[padded_scales.size() - 3] = scale_d; padded_scales[padded_scales.size() - 2] = scale_h; padded_scales[padded_scales.size() - 1] = scale_w; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kLINEAR, align_corners); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -480,7 +480,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kLINEAR, align_corners); } return true; @@ -507,7 +507,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = padded_scales[padded_scales.size() - 3] = scale_d; padded_scales[padded_scales.size() - 2] = scale_h; padded_scales[padded_scales.size() - 1] = scale_w; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kLINEAR, align_corners); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -517,7 +517,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kLINEAR, align_corners); } return true; diff --git a/core/conversion/converters/impl/linear.cpp b/core/conversion/converters/impl/linear.cpp index 6289334736..ad0931452a 100644 --- a/core/conversion/converters/impl/linear.cpp +++ b/core/conversion/converters/impl/linear.cpp @@ -40,22 +40,26 @@ auto linear_registrations TORCHTRT_UNUSED = RegisterNodeConversionPatterns().pat in = in_shuffle->getOutput(0); } - auto w_tensor = args[1].IValue()->toTensor(); - Weights w = Weights(ctx, w_tensor); + // Convert w_tensor to ITensor + auto weight_tensor = args[1].IValue()->toTensor(); + weight_tensor = tensor_to_const(ctx, weight_tensor, util::node_info(n) + "_weight") - nvinfer1::ILayer* new_layer; - if (!args[2].IValue()->isNone()) { - Weights b(ctx, args[2].IValue()->toTensor()); - new_layer = ctx->net->addFullyConnected(*in, w.num_output_maps, w.data, b.data); - } else { - LOG_DEBUG("There is no bias for the linear layer"); - new_layer = ctx->net->addFullyConnected(*in, w.num_output_maps, w.data, Weights().data); - } + auto mm_layer = ctx->net->addMatrixMultiply( + *in, nvinfer1::MatrixOperation::kNONE, *weight_tensor, nvinfer1::MatrixOperation::kNONE); + + TORCHTRT_CHECK(mm_layer, "Unable to create linear layer from node: " << *n); + mm_layer->setName(util::node_info(n).c_str()); - TORCHTRT_CHECK(new_layer, "Unable to create linear layer from node: " << *n); + auto mm_output = mm_layer->getOutput(0); - new_layer->setName(util::node_info(n).c_str()); - auto out_tensor = ctx->AssociateValueAndTensor(n->outputs()[0], new_layer->getOutput(0)); + if (!args[2].IValue()->isNone()) { + // Convert bias to ITensor + auto bias_tensor = args[2].IValue()->toTensor(); + bias_tensor = tensor_to_const(ctx, bias_tensor, util::node_info(n) + "_bias"); + mm_output = add_elementwise( + ctx, nvinfer1::ElementWiseOperation::kSUM, mm_output, bias_tensor, util::node_info(n) + "_bias_add"); + } + auto out_tensor = ctx->AssociateValueAndTensor(n->outputs()[0], mm_output); LOG_DEBUG("Output tensor shape: " << out_tensor->getDimensions()); diff --git a/core/runtime/TRTEngine.cpp b/core/runtime/TRTEngine.cpp index 92e5d7a8ff..aa1ef7fc31 100644 --- a/core/runtime/TRTEngine.cpp +++ b/core/runtime/TRTEngine.cpp @@ -120,16 +120,25 @@ TRTEngine::TRTEngine( } else { uint64_t inputs_size = _in_binding_names.size(); in_binding_names.resize(inputs_size); - for (size_t pyt_idx = 0; pyt_idx < inputs_size; pyt_idx++) { + for (uint64_t pyt_idx = 0; pyt_idx < inputs_size; pyt_idx++) { auto binding_name = _in_binding_names[pyt_idx]; - auto trt_idx = cuda_engine->getBindingIndex(binding_name.c_str()); - std::string engine_binded_name = cuda_engine->getIOTensorName(trt_idx); - TORCHTRT_CHECK( - (binding_name == engine_binded_name), - "Could not find a TensorRT engine binding for input named " << binding_name); + // Check if the binding name provided is in the list of engine's bindings + // by iterating through nbIOTensors and verify it is an input binding + bool is_binding = false, is_input = false; + int32_t trt_idx; + for (int32_t idx = 0; idx < cuda_engine->getNbIOTensors(); idx++) { + std::string curr_bind_name = cuda_engine->getIOTensorName(idx); + if (curr_bind_name == binding_name) { + is_binding = true; + trt_idx = idx; + if (cuda_engine->getTensorIOMode(binding_name.c_str()) == nvinfer1::TensorIOMode::kINPUT) { + is_input = true; + } + } + } + TORCHTRT_CHECK(is_binding, "Could not find a TensorRT engine binding for input named " << binding_name); TORCHTRT_CHECK( - (cuda_engine->getTensorIOMode(binding_name.c_str()) == nvinfer1::TensorIOMode::kINPUT), - "Binding " << binding_name << " specified as input but found as output in TensorRT engine"); + is_input, "Binding " << binding_name << " specified as input but found as output in TensorRT engine"); LOG_DEBUG( "Input binding name: " << binding_name << " has TensorRT binding index: " << trt_idx << ", Torch binding index: " << pyt_idx); @@ -141,11 +150,25 @@ TRTEngine::TRTEngine( out_binding_names.resize(outputs); for (size_t pyt_idx = 0; pyt_idx < outputs; pyt_idx++) { auto binding_name = _out_binding_names[pyt_idx]; - auto trt_idx = cuda_engine->getBindingIndex(binding_name.c_str()); - TORCHTRT_CHECK((trt_idx != -1), "Could not find a TensorRT engine binding for output named " << binding_name); + // Check if the binding name provided is in the list of engine's bindings + // by iterating through nbIOTensors and verify it is an output binding + bool is_binding = false, is_output = false; + int32_t trt_idx; + for (int32_t idx = 0; idx < cuda_engine->getNbIOTensors(); idx++) { + std::string curr_bind_name = cuda_engine->getIOTensorName(idx); + if (curr_bind_name == binding_name) { + is_binding = true; + trt_idx = idx; + if (cuda_engine->getTensorIOMode(binding_name.c_str()) == nvinfer1::TensorIOMode::kOUTPUT) { + is_output = true; + } + } + } + + TORCHTRT_CHECK(is_binding, "Could not find a TensorRT engine binding for output named " << binding_name); TORCHTRT_CHECK( - !(cuda_engine->getTensorIOMode(binding_name.c_str()) == nvinfer1::TensorIOMode::kINPUT), - "Binding " << binding_name << " specified as output but found as input in TensorRT engine"); + is_output, "Binding " << binding_name << " specified as output but found as input in TensorRT engine"); + LOG_DEBUG( "Output binding name: " << binding_name << " has TensorRT binding index: " << trt_idx << ", Torch binding index: " << inputs_size + pyt_idx); From a655c9a29113079b2da04d54ca574d7c97db6b36 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 12 Mar 2024 16:27:16 -0700 Subject: [PATCH 002/122] chore: updates to trt api --- core/conversion/converters/impl/linear.cpp | 16 ++++++++-------- cpp/include/torch_tensorrt/ptq.h | 8 ++++---- py/torch_tensorrt/csrc/torch_tensorrt_py.cpp | 5 +++++ .../dynamo/conversion/_TRTInterpreter.py | 4 ++-- .../dynamo/conversion/_conversion.py | 3 ++- py/torch_tensorrt/dynamo/conversion/impl/conv.py | 2 +- .../dynamo/conversion/impl/deconv.py | 2 +- 7 files changed, 23 insertions(+), 17 deletions(-) diff --git a/core/conversion/converters/impl/linear.cpp b/core/conversion/converters/impl/linear.cpp index ad0931452a..39f9cb055d 100644 --- a/core/conversion/converters/impl/linear.cpp +++ b/core/conversion/converters/impl/linear.cpp @@ -41,11 +41,10 @@ auto linear_registrations TORCHTRT_UNUSED = RegisterNodeConversionPatterns().pat } // Convert w_tensor to ITensor - auto weight_tensor = args[1].IValue()->toTensor(); - weight_tensor = tensor_to_const(ctx, weight_tensor, util::node_info(n) + "_weight") - - auto mm_layer = ctx->net->addMatrixMultiply( - *in, nvinfer1::MatrixOperation::kNONE, *weight_tensor, nvinfer1::MatrixOperation::kNONE); + auto weight = args[1].IValue()->toTensor(); + auto weight_tensor = tensor_to_const(ctx, weight, util::node_info(n) + "_weight"); + auto mm_layer = ctx->net->addMatrixMultiply( + *in, nvinfer1::MatrixOperation::kNONE, *weight_tensor, nvinfer1::MatrixOperation::kNONE); TORCHTRT_CHECK(mm_layer, "Unable to create linear layer from node: " << *n); mm_layer->setName(util::node_info(n).c_str()); @@ -54,10 +53,11 @@ auto linear_registrations TORCHTRT_UNUSED = RegisterNodeConversionPatterns().pat if (!args[2].IValue()->isNone()) { // Convert bias to ITensor - auto bias_tensor = args[2].IValue()->toTensor(); - bias_tensor = tensor_to_const(ctx, bias_tensor, util::node_info(n) + "_bias"); - mm_output = add_elementwise( + auto bias = args[2].IValue()->toTensor(); + auto bias_tensor = tensor_to_const(ctx, bias, util::node_info(n) + "_bias"); + auto bias_add_layer = add_elementwise( ctx, nvinfer1::ElementWiseOperation::kSUM, mm_output, bias_tensor, util::node_info(n) + "_bias_add"); + mm_output = bias_add_layer->getOutput(0); } auto out_tensor = ctx->AssociateValueAndTensor(n->outputs()[0], mm_output); diff --git a/cpp/include/torch_tensorrt/ptq.h b/cpp/include/torch_tensorrt/ptq.h index d8570f0e6e..e88da06b0c 100644 --- a/cpp/include/torch_tensorrt/ptq.h +++ b/cpp/include/torch_tensorrt/ptq.h @@ -21,10 +21,10 @@ #include "torch_tensorrt/macros.h" #ifndef DOXYGEN_SHOULD_SKIP_THIS -namespace nvinfer1 { -class IInt8Calibrator; -class IInt8EntropyCalibrator2; -} // namespace nvinfer1 +// namespace nvinfer1 { +// class IInt8Calibrator; +// class IInt8EntropyCalibrator2; +// } // namespace nvinfer1 namespace torch_tensorrt { namespace ptq { diff --git a/py/torch_tensorrt/csrc/torch_tensorrt_py.cpp b/py/torch_tensorrt/csrc/torch_tensorrt_py.cpp index 33c7e27398..c0655d9907 100644 --- a/py/torch_tensorrt/csrc/torch_tensorrt_py.cpp +++ b/py/torch_tensorrt/csrc/torch_tensorrt_py.cpp @@ -2,6 +2,7 @@ #include "pybind11/stl.h" #include "ATen/core/jit_type.h" +#include "NvInferRuntimeBase.h" #include "Python.h" #include "core/compiler.h" #include "core/conversion/conversion.h" @@ -77,6 +78,10 @@ class pyIInt8Calibrator : public pyCalibratorTrampoline; using Derived::Derived; + nvinfer1::InterfaceInfo getInterfaceInfo() const noexcept override { + return nvinfer1::InterfaceInfo{"PYTHON CALIBRATOR", 1, 0}; + } + nvinfer1::CalibrationAlgoType getAlgorithm() noexcept override { try { PYBIND11_OVERLOAD_PURE_NAME( diff --git a/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py b/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py index 06ae596ed0..8b9d730a72 100644 --- a/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py +++ b/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py @@ -252,7 +252,7 @@ def run( if tactic_sources is not None: builder_config.set_tactic_sources(tactic_sources=tactic_sources) - engine = self.builder.build_engine(self.ctx.net, builder_config) + engine = self.builder.build_serialized_network(self.ctx.net, builder_config) assert engine serialized_cache = ( @@ -263,7 +263,7 @@ def run( _LOGGER.info( f"Build TRT engine elapsed time: {datetime.now() - build_engine_start_time}" ) - _LOGGER.info(f"TRT Engine uses: {engine.device_memory_size} bytes of Memory") + _LOGGER.info(f"TRT Engine uses: {engine.nbytes} bytes of Memory") return TRTInterpreterResult( engine, self._input_names, self._output_names, serialized_cache diff --git a/py/torch_tensorrt/dynamo/conversion/_conversion.py b/py/torch_tensorrt/dynamo/conversion/_conversion.py index 844cb6789a..f1f4af9236 100644 --- a/py/torch_tensorrt/dynamo/conversion/_conversion.py +++ b/py/torch_tensorrt/dynamo/conversion/_conversion.py @@ -87,8 +87,9 @@ def convert_module( from torch_tensorrt.dynamo.runtime import TorchTensorRTModule with io.BytesIO() as engine_bytes: - engine_bytes.write(interpreter_result.engine.serialize()) + engine_bytes.write(interpreter_result.engine) engine_str = engine_bytes.getvalue() + return TorchTensorRTModule( serialized_engine=engine_str, name=name, diff --git a/py/torch_tensorrt/dynamo/conversion/impl/conv.py b/py/torch_tensorrt/dynamo/conversion/impl/conv.py index 26e0d59b8f..6c15b4b5fe 100644 --- a/py/torch_tensorrt/dynamo/conversion/impl/conv.py +++ b/py/torch_tensorrt/dynamo/conversion/impl/conv.py @@ -63,7 +63,7 @@ def convNd( ) # Process weight terms - if ctx.net.has_explicit_precision or isinstance(weight, TRTTensor): + if isinstance(weight, TRTTensor): weight = get_trt_tensor(ctx, weight, f"{name}_weight") # Append new dimension (unsqueeze) if the convolution is 1d if is_conv1d: diff --git a/py/torch_tensorrt/dynamo/conversion/impl/deconv.py b/py/torch_tensorrt/dynamo/conversion/impl/deconv.py index f66bff7c82..03a209e2a5 100644 --- a/py/torch_tensorrt/dynamo/conversion/impl/deconv.py +++ b/py/torch_tensorrt/dynamo/conversion/impl/deconv.py @@ -63,7 +63,7 @@ def deconvNd( ) # Process weight terms - if ctx.net.has_explicit_precision or isinstance(weight, TRTTensor): + if isinstance(weight, TRTTensor): weight = get_trt_tensor(ctx, weight, f"{name}_weight") # Append new dimension (unsqueeze) if the deconvolution is 1d if is_deconv1d: From cd866609a18fb0dffe1e798a1831bcf698095de5 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Thu, 14 Mar 2024 02:01:07 -0700 Subject: [PATCH 003/122] feat: Add save API for torch-trt compiled models --- .github/scripts/install-torch-tensorrt.sh | 3 +- py/torch_tensorrt/_compile.py | 67 +++++++++++++++++++++ py/torch_tensorrt/dynamo/_compiler.py | 9 +-- py/torch_tensorrt/dynamo/_defaults.py | 1 - py/torch_tensorrt/dynamo/_exporter.py | 17 +----- py/torch_tensorrt/dynamo/_settings.py | 3 - tests/py/dynamo/models/test_export_serde.py | 58 +++++++++--------- 7 files changed, 104 insertions(+), 54 deletions(-) diff --git a/.github/scripts/install-torch-tensorrt.sh b/.github/scripts/install-torch-tensorrt.sh index 2930421d5b..9757fadeb4 100644 --- a/.github/scripts/install-torch-tensorrt.sh +++ b/.github/scripts/install-torch-tensorrt.sh @@ -2,7 +2,8 @@ set -eou pipefail # Source conda so it's available to the script environment source ${BUILD_ENV_FILE} -${CONDA_RUN} ${PIP_INSTALL_TORCH} torchvision pyyaml +${CONDA_RUN} ${PIP_INSTALL_TORCH} torchvision --extra-index-url https://pypi.python.org/simple +${CONDA_RUN} python -m pip install pyyaml mpmath==1.3.0 export TRT_VERSION=$(${CONDA_RUN} python -c "import versions; versions.tensorrt_version()") ${CONDA_RUN} python -m pip install /opt/torch-tensorrt-builds/torch_tensorrt*+${CU_VERSION}*.whl tensorrt~=${TRT_VERSION} tensorrt-bindings~=${TRT_VERSION} --extra-index-url=https://pypi.ngc.nvidia.com diff --git a/py/torch_tensorrt/_compile.py b/py/torch_tensorrt/_compile.py index 9dd816e633..aa1bc53a0a 100644 --- a/py/torch_tensorrt/_compile.py +++ b/py/torch_tensorrt/_compile.py @@ -6,6 +6,7 @@ import torch import torch.fx +import torch_tensorrt.dynamo import torch_tensorrt.ts from torch_tensorrt._enums import dtype from torch_tensorrt._Input import Input @@ -29,6 +30,7 @@ __all__ = [ "compile", "convert_method_to_trt_engine", + "save", ] @@ -332,3 +334,68 @@ def convert_method_to_trt_engine( ) else: raise RuntimeError("Module is an unknown format or the ir requested is unknown") + + +def save( + module: Any, + file_path: str = "", + *, + output_format: str = "exported_program", + inputs: Optional[Sequence[torch.Tensor]] = None, + retrace: bool = False, +) -> None: + """ + Save the model to disk in the specified output format. + Arguments: + module : Compiled Torch-TensorRT module (Options include torch.jit.ScriptModule | torch.export.ExportedProgram | torch.fx.GraphModule) + inputs (torch.Tensor): Torch input tensors + """ + module_type = _parse_module_type(module) + accepted_formats = {"exported_program", "torchscript"} + if inputs and not all(isinstance(input, torch.Tensor) for input in inputs): + raise ValueError( + "Not all inputs provided are torch.tensors. Please provide torch.tensors as inputs" + ) + if output_format not in accepted_formats: + raise ValueError( + f"Provided output_format {output_format} is not supported. Supported options are exported_program | torchscript" + ) + if not file_path: + raise ValueError("File path cannot be empty. Please provide a valid file path") + + if module_type == _ModuleType.nn: + raise ValueError( + "Input model is of type nn.Module. Saving nn.Module directly is not supported. Supported model types torch.jit.ScriptModule | torch.fx.GraphModule | torch.export.ExportedProgram." + ) + elif module_type == _ModuleType.ts: + if output_format == "exported_program": + raise ValueError( + "Provided model is a torch.jit.ScriptModule but the output_format specified is exported_program. Please verify the output_format" + ) + else: + torch.jit.save(module, file_path) + elif module_type == _ModuleType.ep: + if output_format == "torchscript": + raise ValueError( + "Provided model is a torch.export.ExportedProgram but the output_format specified is torchscript. Please verify the output_format" + ) + else: + torch.export.save(module, file_path) + elif module_type == _ModuleType.fx: + if not inputs: + raise ValueError( + "Provided model is a torch.fx.GraphModule however the inputs are empty. Please provide valid torch.tensors as inputs to trace and save the model" + ) + # The module type is torch.fx.GraphModule + if output_format == "torchscript": + module_ts = torch.jit.trace(module, inputs) + torch.jit.save(module_ts, file_path) + else: + if not retrace: + from torch_tensorrt.dynamo._exporter import export + + exp_program = export(module, inputs) + torch.export.save(exp_program, file_path) + else: + exp_program = torch.export.export(module, tuple(inputs), strict=False) + torch.export.save(exp_program, file_path) diff --git a/py/torch_tensorrt/dynamo/_compiler.py b/py/torch_tensorrt/dynamo/_compiler.py index 6312532f1c..b321eabcb2 100644 --- a/py/torch_tensorrt/dynamo/_compiler.py +++ b/py/torch_tensorrt/dynamo/_compiler.py @@ -30,7 +30,6 @@ MIN_BLOCK_SIZE, NUM_AVG_TIMING_ITERS, OPTIMIZATION_LEVEL, - OUTPUT_FORMAT, PASS_THROUGH_BUILD_FAILURES, PRECISION, REFIT, @@ -48,7 +47,6 @@ dryrun_stats_display, parse_non_trt_nodes, ) -from torch_tensorrt.dynamo._exporter import export from torch_tensorrt.dynamo.conversion import ( CompilationSettings, UnsupportedOperatorException, @@ -102,9 +100,8 @@ def compile( enable_experimental_decompositions: bool = ENABLE_EXPERIMENTAL_DECOMPOSITIONS, dryrun: bool = DRYRUN, hardware_compatible: bool = HARDWARE_COMPATIBLE, - output_format: str = OUTPUT_FORMAT, **kwargs: Any, -) -> Union[ExportedProgram, torch.jit.ScriptModule, torch.fx.GraphModule]: +) -> torch.fx.GraphModule: """Compile a TorchScript module for NVIDIA GPUs using TensorRT Takes a existing TorchScript module and a set of settings to configure the compiler @@ -246,14 +243,12 @@ def compile( "dla_global_dram_size": dla_global_dram_size, "dryrun": dryrun, "hardware_compatible": hardware_compatible, - "output_format": output_format, } settings = CompilationSettings(**compilation_options) logger.info("Compilation Settings: %s\n", settings) trt_gm = compile_module(gm, inputs, settings) - trt_result = export(trt_gm, torch_inputs, output_format) - return trt_result + return trt_gm def compile_module( diff --git a/py/torch_tensorrt/dynamo/_defaults.py b/py/torch_tensorrt/dynamo/_defaults.py index ec038c0dba..3d48ab3def 100644 --- a/py/torch_tensorrt/dynamo/_defaults.py +++ b/py/torch_tensorrt/dynamo/_defaults.py @@ -26,7 +26,6 @@ REQUIRE_FULL_COMPILATION = False DRYRUN = False HARDWARE_COMPATIBLE = False -OUTPUT_FORMAT = "exported_program" def default_device() -> Device: diff --git a/py/torch_tensorrt/dynamo/_exporter.py b/py/torch_tensorrt/dynamo/_exporter.py index c7e2f37795..bae20ac235 100644 --- a/py/torch_tensorrt/dynamo/_exporter.py +++ b/py/torch_tensorrt/dynamo/_exporter.py @@ -18,27 +18,16 @@ def export( gm: torch.fx.GraphModule, inputs: Sequence[torch.Tensor], - output_format: str, ) -> ExportedProgram: """Export the result of TensorRT compilation into the desired output format. Arguments: gm (torch.fx.GraphModule): Compiled Torch-TensorRT module, generated by ``torch_tensorrt.dynamo.compile`` inputs (torch.Tensor): Torch input tensors - output_format (str): Output format of the result of TRT compilation. Options include "exported_program" (or) "ep" | "torchscript" (or) "ts" | "graph_module" (or) "fx". Default is "exported_program" """ - if output_format == "torchscript" or output_format == "ts": - return torch.jit.trace(gm, inputs) - elif output_format == "exported_program" or output_format == "ep": - patched_module = transform(gm, inputs) - exp_program = create_trt_exp_program(patched_module) - return exp_program - elif output_format == "graph_module" or output_format == "fx": - return gm - else: - raise ValueError( - f"Invalid output format {output_format} specified. Supported options include exported_program (or) ep | torchscript (or) ts | graph_module (or) fx" - ) + patched_module = transform(gm, inputs) + exp_program = create_trt_exp_program(patched_module) + return exp_program def transform( diff --git a/py/torch_tensorrt/dynamo/_settings.py b/py/torch_tensorrt/dynamo/_settings.py index c00b049f45..2420a227d8 100644 --- a/py/torch_tensorrt/dynamo/_settings.py +++ b/py/torch_tensorrt/dynamo/_settings.py @@ -19,7 +19,6 @@ MIN_BLOCK_SIZE, NUM_AVG_TIMING_ITERS, OPTIMIZATION_LEVEL, - OUTPUT_FORMAT, PASS_THROUGH_BUILD_FAILURES, PRECISION, REFIT, @@ -71,7 +70,6 @@ class CompilationSettings: TRT Engines. Prints detailed logs of the graph structure and nature of partitioning. Optionally saves the ouptut to a file if a string path is specified hardware_compatible (bool): Build the TensorRT engines compatible with GPU architectures other than that of the GPU on which the engine was built (currently works for NVIDIA Ampere and newer) - output_format (str): Output format of the result of TRT compilation. Options include "exported_program" (or) "ep" | "torchscript" (or) "ts" | "graph_module" (or) "fx". Default is "exported_program" """ precision: torch.dtype = PRECISION @@ -99,4 +97,3 @@ class CompilationSettings: dla_global_dram_size: int = DLA_GLOBAL_DRAM_SIZE dryrun: Union[bool, str] = DRYRUN hardware_compatible: bool = HARDWARE_COMPATIBLE - output_format: str = OUTPUT_FORMAT diff --git a/tests/py/dynamo/models/test_export_serde.py b/tests/py/dynamo/models/test_export_serde.py index efa593890e..0905c5e859 100644 --- a/tests/py/dynamo/models/test_export_serde.py +++ b/tests/py/dynamo/models/test_export_serde.py @@ -42,18 +42,18 @@ def forward(self, x): } exp_program = torchtrt.dynamo.trace(model, **compile_spec) - trt_exp_program = torchtrt.dynamo.compile(exp_program, **compile_spec) - torch.export.save(trt_exp_program, "/tmp/trt.ep") + trt_module = torchtrt.dynamo.compile(exp_program, **compile_spec) + torchtrt.save(trt_module, "/tmp/trt.ep", inputs=[input]) deser_trt_exp_program = torch.export.load("/tmp/trt.ep") - + deser_trt_module = deser_trt_exp_program.module() # Check Pyt and TRT exported program outputs - cos_sim = cosine_similarity(model(input), trt_exp_program(input)[0]) + cos_sim = cosine_similarity(model(input), trt_module(input)[0]) assertions.assertTrue( cos_sim > COSINE_THRESHOLD, msg=f"test_base_model_full_compile TRT outputs don't match with the original model. Cosine sim score: {cos_sim} Threshold: {COSINE_THRESHOLD}", ) # Check Pyt and deserialized TRT exported program outputs - cos_sim = cosine_similarity(model(input), deser_trt_exp_program(input)[0]) + cos_sim = cosine_similarity(model(input), deser_trt_module(input)[0]) assertions.assertTrue( cos_sim > COSINE_THRESHOLD, msg=f"test_base_model_full_compile TRT outputs don't match with the original model. Cosine sim score: {cos_sim} Threshold: {COSINE_THRESHOLD}", @@ -93,12 +93,13 @@ def forward(self, x): } exp_program = torchtrt.dynamo.trace(model, **compile_spec) - trt_exp_program = torchtrt.dynamo.compile(exp_program, **compile_spec) - torch.export.save(trt_exp_program, "/tmp/trt.ep") + trt_module = torchtrt.dynamo.compile(exp_program, **compile_spec) + torchtrt.save(trt_module, "/tmp/trt.ep", inputs=[input]) deser_trt_exp_program = torch.export.load("/tmp/trt.ep") + deser_trt_module = deser_trt_exp_program.module() # Check Pyt and TRT exported program outputs outputs_pyt = model(input) - outputs_trt = trt_exp_program(input) + outputs_trt = trt_module(input) for idx in range(len(outputs_pyt)): cos_sim = cosine_similarity(outputs_pyt[idx], outputs_trt[idx]) assertions.assertTrue( @@ -107,7 +108,7 @@ def forward(self, x): ) # Check Pyt and deserialized TRT exported program outputs - outputs_trt_deser = deser_trt_exp_program(input) + outputs_trt_deser = deser_trt_module(input) for idx in range(len(outputs_pyt)): cos_sim = cosine_similarity(outputs_pyt[idx], outputs_trt_deser[idx]) assertions.assertTrue( @@ -149,12 +150,13 @@ def forward(self, x): } exp_program = torchtrt.dynamo.trace(model, **compile_spec) - trt_exp_program = torchtrt.dynamo.compile(exp_program, **compile_spec) - torch.export.save(trt_exp_program, "/tmp/trt.ep") + trt_module = torchtrt.dynamo.compile(exp_program, **compile_spec) + torchtrt.save(trt_module, "/tmp/trt.ep", inputs=[input]) deser_trt_exp_program = torch.export.load("/tmp/trt.ep") + deser_trt_module = deser_trt_exp_program.module() # Check Pyt and TRT exported program outputs outputs_pyt = model(input) - outputs_trt = trt_exp_program(input) + outputs_trt = trt_module(input) for idx in range(len(outputs_pyt)): cos_sim = cosine_similarity(outputs_pyt[idx], outputs_trt[idx]) assertions.assertTrue( @@ -163,7 +165,7 @@ def forward(self, x): ) # Check Pyt and deserialized TRT exported program outputs - outputs_trt_deser = deser_trt_exp_program(input) + outputs_trt_deser = deser_trt_module(input) for idx in range(len(outputs_pyt)): cos_sim = cosine_similarity(outputs_pyt[idx], outputs_trt_deser[idx]) assertions.assertTrue( @@ -207,12 +209,12 @@ def forward(self, x): } exp_program = torchtrt.dynamo.trace(model, **compile_spec) - trt_exp_program = torchtrt.dynamo.compile(exp_program, **compile_spec) - torch.export.save(trt_exp_program, "/tmp/trt.ep") + trt_module = torchtrt.dynamo.compile(exp_program, **compile_spec) + torchtrt.save(trt_module, "/tmp/trt.ep", inputs=[input]) deser_trt_exp_program = torch.export.load("/tmp/trt.ep") - + deser_trt_module = deser_trt_exp_program.module() outputs_pyt = model(input) - outputs_trt = trt_exp_program(input) + outputs_trt = trt_module(input) for idx in range(len(outputs_pyt)): cos_sim = cosine_similarity(outputs_pyt[idx], outputs_trt[idx]) assertions.assertTrue( @@ -220,7 +222,7 @@ def forward(self, x): msg=f"test_hybrid_relu_fallback TRT outputs don't match with the original model. Cosine sim score: {cos_sim} Threshold: {COSINE_THRESHOLD}", ) - outputs_trt_deser = deser_trt_exp_program(input) + outputs_trt_deser = deser_trt_module(input) for idx in range(len(outputs_pyt)): cos_sim = cosine_similarity(outputs_pyt[idx], outputs_trt_deser[idx]) assertions.assertTrue( @@ -248,19 +250,19 @@ def test_resnet18(ir): } exp_program = torchtrt.dynamo.trace(model, **compile_spec) - trt_exp_program = torchtrt.dynamo.compile(exp_program, **compile_spec) - torch.export.save(trt_exp_program, "/tmp/trt.ep") + trt_module = torchtrt.dynamo.compile(exp_program, **compile_spec) + torchtrt.save(trt_module, "/tmp/trt.ep", inputs=[input]) deser_trt_exp_program = torch.export.load("/tmp/trt.ep") - + deser_trt_module = deser_trt_exp_program.module() outputs_pyt = model(input) - outputs_trt = trt_exp_program(input) + outputs_trt = trt_module(input) cos_sim = cosine_similarity(outputs_pyt, outputs_trt[0]) assertions.assertTrue( cos_sim > COSINE_THRESHOLD, msg=f"test_resnet18 TRT outputs don't match with the original model. Cosine sim score: {cos_sim} Threshold: {COSINE_THRESHOLD}", ) - outputs_trt_deser = deser_trt_exp_program(input) + outputs_trt_deser = deser_trt_module(input) cos_sim = cosine_similarity(outputs_pyt, outputs_trt_deser[0]) assertions.assertTrue( @@ -303,12 +305,12 @@ def forward(self, x): } exp_program = torchtrt.dynamo.trace(model, **compile_spec) - trt_exp_program = torchtrt.dynamo.compile(exp_program, **compile_spec) - torch.export.save(trt_exp_program, "/tmp/trt.ep") + trt_module = torchtrt.dynamo.compile(exp_program, **compile_spec) + torchtrt.save(trt_module, "/tmp/trt.ep", inputs=[input]) deser_trt_exp_program = torch.export.load("/tmp/trt.ep") - + deser_trt_module = deser_trt_exp_program.module() outputs_pyt = model(input) - outputs_trt = trt_exp_program(input) + outputs_trt = trt_module(input) for idx in range(len(outputs_pyt)): cos_sim = cosine_similarity(outputs_pyt[idx], outputs_trt[idx]) @@ -317,7 +319,7 @@ def forward(self, x): msg=f"test_hybrid_conv_fallback TRT outputs don't match with the original model. Cosine sim score: {cos_sim} Threshold: {COSINE_THRESHOLD}", ) - outputs_trt_deser = deser_trt_exp_program(input) + outputs_trt_deser = deser_trt_module(input) for idx in range(len(outputs_pyt)): cos_sim = cosine_similarity(outputs_pyt[idx], outputs_trt_deser[idx]) assertions.assertTrue( From 31285e5709f8789a6532820df101599bb50c2c0d Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 4 Mar 2024 17:29:42 -0800 Subject: [PATCH 004/122] feat: Add FP8 support including dtype and converters --- py/torch_tensorrt/dynamo/_compiler.py | 13 ++++-- py/torch_tensorrt/dynamo/backend/backends.py | 4 +- .../dynamo/conversion/aten_ops_converters.py | 18 ++++++++ .../dynamo/conversion/impl/__init__.py | 1 + .../dynamo/conversion/impl/quantize.py | 43 +++++++++++++++++++ py/torch_tensorrt/dynamo/lowering/__init__.py | 2 +- .../lowering/passes/_aten_lowering_pass.py | 41 +++++++++++++----- .../dynamo/lowering/passes/remove_detach.py | 32 ++++++++++++++ 8 files changed, 137 insertions(+), 17 deletions(-) create mode 100644 py/torch_tensorrt/dynamo/conversion/impl/quantize.py create mode 100644 py/torch_tensorrt/dynamo/lowering/passes/remove_detach.py diff --git a/py/torch_tensorrt/dynamo/_compiler.py b/py/torch_tensorrt/dynamo/_compiler.py index 6312532f1c..1a635b5af5 100644 --- a/py/torch_tensorrt/dynamo/_compiler.py +++ b/py/torch_tensorrt/dynamo/_compiler.py @@ -59,7 +59,11 @@ from torch_tensorrt.dynamo.conversion._ConverterRegistry import ( DYNAMO_CONVERTERS as CONVERTERS, ) -from torch_tensorrt.dynamo.lowering import apply_lowering_passes, get_decompositions +from torch_tensorrt.dynamo.lowering import ( + get_decompositions, + post_lowering, + pre_export_lowering, +) from torch_tensorrt.dynamo.utils import ( get_torch_inputs, parse_complex_tensor_structs, @@ -181,12 +185,15 @@ def compile( # Prepare torch_trt inputs inputs = prepare_inputs(inputs) + torch_inputs = get_torch_inputs(inputs, device) device = to_torch_tensorrt_device(device) if not isinstance(exported_program, ExportedProgram): raise AssertionError( f"Input graph should be an ExportedProgram but got type {type(exported_program)}" ) + + exported_program = pre_export_lowering(exported_program, torch_inputs) exported_program = exported_program.run_decompositions( get_decompositions(enable_experimental_decompositions) ) @@ -194,10 +201,8 @@ def compile( logger.debug("Input graph: " + str(gm.graph)) # Apply lowering on the graph module - torch_inputs = get_torch_inputs(inputs, device) - gm = apply_lowering_passes(gm, torch_inputs) + gm = post_lowering(gm, torch_inputs) logger.debug("Lowered Input graph: " + str(gm.graph)) - enabled_precisions = set(enabled_precisions) if ( diff --git a/py/torch_tensorrt/dynamo/backend/backends.py b/py/torch_tensorrt/dynamo/backend/backends.py index 1fa2806181..961e1c9344 100644 --- a/py/torch_tensorrt/dynamo/backend/backends.py +++ b/py/torch_tensorrt/dynamo/backend/backends.py @@ -11,8 +11,8 @@ from torch_tensorrt.dynamo import CompilationSettings from torch_tensorrt.dynamo._compiler import compile_module from torch_tensorrt.dynamo.lowering import ( - apply_lowering_passes, get_decompositions, + post_lowering, repair_input_aliasing, ) from torch_tensorrt.dynamo.utils import ( @@ -87,7 +87,7 @@ def _pretraced_backend( logger.debug("Post-AOT Autograd graph:\n" + str(gm.graph)) - gm = apply_lowering_passes(gm, sample_inputs) + gm = post_lowering(gm, sample_inputs) torchtrt_inputs = prepare_inputs( sample_inputs, disable_memory_format_check=True diff --git a/py/torch_tensorrt/dynamo/conversion/aten_ops_converters.py b/py/torch_tensorrt/dynamo/conversion/aten_ops_converters.py index 45949a1c8d..44ccf0546b 100644 --- a/py/torch_tensorrt/dynamo/conversion/aten_ops_converters.py +++ b/py/torch_tensorrt/dynamo/conversion/aten_ops_converters.py @@ -573,6 +573,24 @@ def aten_ops_neg( ) +@dynamo_tensorrt_converter(torch.ops.ptq.scaled_e4m3.default) +def aten_ops_quantize_fp8( + ctx: ConversionContext, + target: Target, + args: Tuple[Argument, ...], + kwargs: Dict[str, Argument], + name: str, +) -> Union[TRTTensor, Sequence[TRTTensor]]: + return impl.quantize.quantize_fp8( + ctx, + target, + SourceIR.ATEN, + name, + args[0], + args[1], + ) + + @dynamo_tensorrt_converter(torch.ops.aten.squeeze.dim) @dynamo_tensorrt_converter(torch.ops.aten.squeeze.dims) def aten_ops_squeeze( diff --git a/py/torch_tensorrt/dynamo/conversion/impl/__init__.py b/py/torch_tensorrt/dynamo/conversion/impl/__init__.py index ca71cb0b0c..a18155d6be 100644 --- a/py/torch_tensorrt/dynamo/conversion/impl/__init__.py +++ b/py/torch_tensorrt/dynamo/conversion/impl/__init__.py @@ -18,6 +18,7 @@ pad, permutation, pool, + quantize, reduce, select, shape, diff --git a/py/torch_tensorrt/dynamo/conversion/impl/quantize.py b/py/torch_tensorrt/dynamo/conversion/impl/quantize.py new file mode 100644 index 0000000000..f5c3a4cd98 --- /dev/null +++ b/py/torch_tensorrt/dynamo/conversion/impl/quantize.py @@ -0,0 +1,43 @@ +from typing import Optional + +import numpy as np +import tensorrt as trt +from torch.fx.node import Target +from torch_tensorrt.dynamo._SourceIR import SourceIR +from torch_tensorrt.dynamo.conversion._ConversionContext import ConversionContext +from torch_tensorrt.dynamo.conversion.converter_utils import get_trt_tensor +from torch_tensorrt.fx.converters.converter_utils import set_layer_name +from torch_tensorrt.fx.types import TRTTensor + + +def quantize_fp8( + ctx: ConversionContext, + target: Target, + source_ir: Optional[SourceIR], + name: str, + input_tensor: TRTTensor, + scale: np.ndarray, +) -> TRTTensor: + """ + Adds quantize and dequantize ops (QDQ) which quantize to INT8 or FP8 based + on the output_type set and dequantizes them back. + """ + if (isinstance(input_tensor, TRTTensor)) and not ( + input_tensor.dtype != trt.float32 or input_tensor.dtype != trt.float16 + ): + raise ValueError( + f"quantize_fp8 converter received an input of {input_tensor.dtype} type. Supported types: float32 | float16" + ) + + if isinstance(scale, np.ndarray): + scale = get_trt_tensor(ctx, scale, name + "_scale") + # Add Q node + quantize_layer = ctx.net.add_quantize(input_tensor, scale) + set_layer_name(quantize_layer, target, name + "_quantize", source_ir) + q_output = quantize_layer.get_output(0) + # Add DQ node + dequantize_layer = ctx.net.add_dequantize(q_output, scale) + set_layer_name(dequantize_layer, target, name + "_dequantize", source_ir) + dq_output = dequantize_layer.get_output(0) + + return dq_output diff --git a/py/torch_tensorrt/dynamo/lowering/__init__.py b/py/torch_tensorrt/dynamo/lowering/__init__.py index 7c4e9fdd2d..67a73354ad 100644 --- a/py/torch_tensorrt/dynamo/lowering/__init__.py +++ b/py/torch_tensorrt/dynamo/lowering/__init__.py @@ -5,4 +5,4 @@ from ._decompositions import get_decompositions # noqa: F401 from ._fusers import * # noqa: F401 from ._repair_input_aliasing import repair_input_aliasing -from .passes import apply_lowering_passes +from .passes import post_lowering, pre_export_lowering diff --git a/py/torch_tensorrt/dynamo/lowering/passes/_aten_lowering_pass.py b/py/torch_tensorrt/dynamo/lowering/passes/_aten_lowering_pass.py index 489805cb43..05a171e264 100644 --- a/py/torch_tensorrt/dynamo/lowering/passes/_aten_lowering_pass.py +++ b/py/torch_tensorrt/dynamo/lowering/passes/_aten_lowering_pass.py @@ -8,12 +8,13 @@ from .lower_linear import lower_linear from .lower_scaled_dot_product_attention import lower_scaled_dot_product_attention from .pass_manager import DynamoPassManager +from .remove_detach import remove_detach from .remove_input_alias_fixing_clones import remove_input_alias_fixing_clones from .repair_input_as_output import repair_input_as_output from .replace_max_pool_with_indices import replace_max_pool_with_indices from .view_to_reshape import view_to_reshape -ATEN_LOWERING_PASSES = DynamoPassManager.build_from_passlist( +ATEN_POST_LOWERING_PASSES = DynamoPassManager.build_from_passlist( [ remove_input_alias_fixing_clones, constant_fold, @@ -26,6 +27,12 @@ ] ) +ATEN_PRE_LOWERING_PASSES = DynamoPassManager.build_from_passlist( + [ + remove_detach, + ] +) + logger = logging.getLogger(__name__) @@ -48,9 +55,9 @@ def _aten_lowering_pass( def add_lowering_pass( lowering_pass: LoweringPassSignature, ) -> LoweringPassSignature: - ATEN_LOWERING_PASSES.add_pass_with_index(lowering_pass, index) + ATEN_POST_LOWERING_PASSES.add_pass_with_index(lowering_pass, index) logger.debug( - f"Added lowering pass {lowering_pass} to list at index {index}, current passlist: {ATEN_LOWERING_PASSES}" + f"Added lowering pass {lowering_pass} to list at index {index}, current passlist: {ATEN_POST_LOWERING_PASSES}" ) return lowering_pass @@ -72,23 +79,37 @@ def add_lowering_pass( def _remove_lowering_pass(*, index: int) -> None: """Removes a lowering pass at a specific index from the registry""" - ATEN_LOWERING_PASSES.remove_pass_with_index(index) + ATEN_POST_LOWERING_PASSES.remove_pass_with_index(index) logger.debug( - f"Removed lowering pass at index {index}, current passlist: {ATEN_LOWERING_PASSES}" + f"Removed lowering pass at index {index}, current passlist: {ATEN_POST_LOWERING_PASSES}" ) return -def apply_lowering_passes( +def post_lowering( gm: torch.fx.GraphModule, sample_inputs: Sequence[torch.Tensor] ) -> torch.fx.GraphModule: - """Applies the lowering passes to a graph module, returns the modified GraphModule""" + """Applies the lowering passes to a graph module after torch.export/ torch.compile and their decompositions, returns the modified GraphModule""" + logging.debug( + f"Invoking DynamoPassManager and applying lowering passes: {ATEN_POST_LOWERING_PASSES}" + ) + return ATEN_POST_LOWERING_PASSES(gm, sample_inputs) + + +def pre_export_lowering( + ep: torch.export.ExportedProgram, sample_inputs: Sequence[torch.Tensor] +) -> torch.fx.GraphModule: + """Applies the lowering passes to a graph module after torch.export/ torch.compile and their decompositions, returns the modified GraphModule""" logging.debug( - f"Invoking DynamoPassManager and applying lowering passes: {ATEN_LOWERING_PASSES}" + f"Invoking DynamoPassManager and applying lowering passes: {ATEN_PRE_LOWERING_PASSES}" ) - return ATEN_LOWERING_PASSES(gm, sample_inputs) + gm = ep.module() + gm = ATEN_PRE_LOWERING_PASSES(gm, sample_inputs) + # TODO: Check if re-exporting changes the metadata + transformed_ep = torch.export.export(gm, tuple(sample_inputs), strict=False) + return transformed_ep def dump_lowering_passes() -> str: """Returns a string containing the lowering passes""" - return str(ATEN_LOWERING_PASSES) + return str(ATEN_POST_LOWERING_PASSES) diff --git a/py/torch_tensorrt/dynamo/lowering/passes/remove_detach.py b/py/torch_tensorrt/dynamo/lowering/passes/remove_detach.py new file mode 100644 index 0000000000..5c47a8e288 --- /dev/null +++ b/py/torch_tensorrt/dynamo/lowering/passes/remove_detach.py @@ -0,0 +1,32 @@ +import logging +from typing import Sequence + +import torch +from torch_tensorrt.dynamo.lowering.passes.pass_utils import ( + clean_up_graph_after_modifications, +) + +logger = logging.getLogger(__name__) + + +def remove_detach( + gm: torch.fx.GraphModule, sample_inputs: Sequence[torch.Tensor] +) -> torch.fx.GraphModule: + """Remove detach ops in the graph""" + + modified_graph = False + count = 0 + for node in gm.graph.nodes: + # If the node is a detach node + if node.target == torch.ops.aten.detach.default: + # Detach node has only one input + node_input = node.all_input_nodes[0] + node.replace_all_uses_with(node_input) + gm.graph.erase_node(node) + count += 1 + + if modified_graph: + gm = clean_up_graph_after_modifications(gm) + logger.debug(f"Removed {count} detach nodes:\n{gm.graph}") + + return gm From 7c9c6462c3585932c15d965b58a09b215427c207 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Fri, 15 Mar 2024 14:30:10 -0700 Subject: [PATCH 005/122] chore: minor fixes --- py/torch_tensorrt/dynamo/_compiler.py | 4 +- .../dynamo/conversion/_TRTInterpreter.py | 5 ++- .../dynamo/conversion/aten_ops_converters.py | 40 +++++++++++-------- .../dynamo/conversion/impl/quantize.py | 3 ++ 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/py/torch_tensorrt/dynamo/_compiler.py b/py/torch_tensorrt/dynamo/_compiler.py index 1a635b5af5..ed1972ce11 100644 --- a/py/torch_tensorrt/dynamo/_compiler.py +++ b/py/torch_tensorrt/dynamo/_compiler.py @@ -205,7 +205,9 @@ def compile( logger.debug("Lowered Input graph: " + str(gm.graph)) enabled_precisions = set(enabled_precisions) - if ( + if torch.float8_e4m3fn in enabled_precisions: + precision = torch.float8_e4m3fn + elif ( torch.float16 in enabled_precisions or torch_tensorrt.dtype.half in enabled_precisions ): diff --git a/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py b/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py index 8b9d730a72..21b181c0c2 100644 --- a/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py +++ b/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py @@ -172,7 +172,7 @@ def run( if version.parse(trt.__version__) >= version.parse("8.2"): builder_config.profiling_verbosity = ( - trt.ProfilingVerbosity.VERBOSE + trt.ProfilingVerbosity.DETAILED if self.compilation_settings.debug else trt.ProfilingVerbosity.LAYER_NAMES_ONLY ) @@ -228,6 +228,9 @@ def run( if precision == torch.int8: builder_config.set_flag(trt.BuilderFlag.INT8) + if precision == torch.float8_e4m3fn: + builder_config.set_flag(trt.BuilderFlag.FP8) + if self.compilation_settings.sparse_weights: builder_config.set_flag(trt.BuilderFlag.SPARSE_WEIGHTS) diff --git a/py/torch_tensorrt/dynamo/conversion/aten_ops_converters.py b/py/torch_tensorrt/dynamo/conversion/aten_ops_converters.py index 44ccf0546b..46b40bee06 100644 --- a/py/torch_tensorrt/dynamo/conversion/aten_ops_converters.py +++ b/py/torch_tensorrt/dynamo/conversion/aten_ops_converters.py @@ -573,22 +573,30 @@ def aten_ops_neg( ) -@dynamo_tensorrt_converter(torch.ops.ptq.scaled_e4m3.default) -def aten_ops_quantize_fp8( - ctx: ConversionContext, - target: Target, - args: Tuple[Argument, ...], - kwargs: Dict[str, Argument], - name: str, -) -> Union[TRTTensor, Sequence[TRTTensor]]: - return impl.quantize.quantize_fp8( - ctx, - target, - SourceIR.ATEN, - name, - args[0], - args[1], - ) +try: + assert torch.ops.trt.quantize_fp8.default +except Exception as e: + _LOGGER.warn( + f"Unable to import quantize_fp8 op.: {e}. Please install nvidia-ammo library in order to register torch.ops.trt.quantize_fp8 op" + ) +else: + + @dynamo_tensorrt_converter(torch.ops.trt.quantize_fp8.default) + def aten_ops_quantize_fp8( + ctx: ConversionContext, + target: Target, + args: Tuple[Argument, ...], + kwargs: Dict[str, Argument], + name: str, + ) -> Union[TRTTensor, Sequence[TRTTensor]]: + return impl.quantize.quantize_fp8( + ctx, + target, + SourceIR.ATEN, + name, + args[0], + args[1], + ) @dynamo_tensorrt_converter(torch.ops.aten.squeeze.dim) diff --git a/py/torch_tensorrt/dynamo/conversion/impl/quantize.py b/py/torch_tensorrt/dynamo/conversion/impl/quantize.py index f5c3a4cd98..478e8a5771 100644 --- a/py/torch_tensorrt/dynamo/conversion/impl/quantize.py +++ b/py/torch_tensorrt/dynamo/conversion/impl/quantize.py @@ -33,11 +33,14 @@ def quantize_fp8( scale = get_trt_tensor(ctx, scale, name + "_scale") # Add Q node quantize_layer = ctx.net.add_quantize(input_tensor, scale) + quantize_layer.set_output_type(0, trt.DataType.FP8) set_layer_name(quantize_layer, target, name + "_quantize", source_ir) q_output = quantize_layer.get_output(0) # Add DQ node dequantize_layer = ctx.net.add_dequantize(q_output, scale) set_layer_name(dequantize_layer, target, name + "_dequantize", source_ir) + # Set DQ layer precision to FP8 + dequantize_layer.precision = trt.DataType.FP8 dq_output = dequantize_layer.get_output(0) return dq_output From eab0dba2955a87550fe12e7b67ae092597b8c453 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 18 Mar 2024 12:43:45 -0700 Subject: [PATCH 006/122] chore: Fix save failures --- core/runtime/TRTEngine.cpp | 2 +- py/torch_tensorrt/_compile.py | 9 +- py/torch_tensorrt/dynamo/_exporter.py | 135 +++++++++++++------- tests/py/dynamo/models/test_export_serde.py | 2 +- 4 files changed, 100 insertions(+), 48 deletions(-) diff --git a/core/runtime/TRTEngine.cpp b/core/runtime/TRTEngine.cpp index 92e5d7a8ff..7a046f6d94 100644 --- a/core/runtime/TRTEngine.cpp +++ b/core/runtime/TRTEngine.cpp @@ -241,7 +241,7 @@ std::string TRTEngine::to_str() const { exec_ctx->getEngine().getTensorDataType(out_binding_names[o].c_str())) << std::endl; } - ss << " }" << std::endl; + ss << " ]" << std::endl; ss << " Device: " << device_info << std::endl; ss << " Hardware Compatibility: " << (hardware_compatible ? "Enabled" : "Disabled") << std::endl; // clang-format on diff --git a/py/torch_tensorrt/_compile.py b/py/torch_tensorrt/_compile.py index aa1bc53a0a..443dec8869 100644 --- a/py/torch_tensorrt/_compile.py +++ b/py/torch_tensorrt/_compile.py @@ -397,5 +397,10 @@ def save( exp_program = export(module, inputs) torch.export.save(exp_program, file_path) else: - exp_program = torch.export.export(module, tuple(inputs), strict=False) - torch.export.save(exp_program, file_path) + from torch._higher_order_ops.torchbind import enable_torchbind_tracing + + with enable_torchbind_tracing(): + exp_program = torch.export.export( + module, tuple(inputs), strict=False + ) + torch.export.save(exp_program, file_path) diff --git a/py/torch_tensorrt/dynamo/_exporter.py b/py/torch_tensorrt/dynamo/_exporter.py index bae20ac235..cf06bc4531 100644 --- a/py/torch_tensorrt/dynamo/_exporter.py +++ b/py/torch_tensorrt/dynamo/_exporter.py @@ -1,3 +1,4 @@ +import copy import operator from typing import Any, Dict, Sequence, Tuple, cast @@ -6,8 +7,11 @@ from torch._subclasses.fake_tensor import FakeTensor from torch.export import ExportedProgram, ExportGraphSignature from torch.export.exported_program import ( + CustomObjArgument, InputKind, InputSpec, + ModuleCallEntry, + ModuleCallSignature, OutputKind, OutputSpec, TensorArgument, @@ -44,24 +48,27 @@ def transform( Returns an inlined torch.fx.GraphModule """ + gm_export = copy.deepcopy(gm) # Run shape analysis - _, outputs_map = partitioning.run_shape_analysis(gm, inputs) + _, outputs_map = partitioning.run_shape_analysis(gm_export, inputs) # Inline TensorRT submodules - inline_trt_modules(gm, outputs_map) + inline_trt_modules(gm_export, outputs_map) # Inline pytorch submodules - inline_torch_modules(gm) + inline_torch_modules(gm_export) # Clean the graph - gm.delete_all_unused_submodules() - gm.graph.eliminate_dead_code() - gm.graph.lint() + gm_export.delete_all_unused_submodules() + gm_export.graph.eliminate_dead_code() + gm_export.graph.lint() - return gm + return gm_export -def lift(gm: torch.fx.GraphModule, graph_signature: Any) -> torch.fx.GraphModule: +def lift( + gm: torch.fx.GraphModule, graph_signature: Any +) -> Tuple[torch.fx.GraphModule, ExportGraphSignature, Dict[str, Any], Dict[str, Any]]: """ Given an unlifted fx.GraphModule, lift all parameters, buffers into placeholders. Arguments: @@ -75,6 +82,7 @@ def lift(gm: torch.fx.GraphModule, graph_signature: Any) -> torch.fx.GraphModule # exp_program.state_dict contains parameters and buffers whereas a graph_module's state_dict # has all parameters registered as torch.tensors. state_dict = gm.state_dict() + constants = {} fake_mode = detect_fake_mode( tuple(node.meta["val"] for node in gm.graph.nodes if node.op == "placeholder") @@ -89,52 +97,68 @@ def lift(gm: torch.fx.GraphModule, graph_signature: Any) -> torch.fx.GraphModule break # At first the user_inputs are only present in the graph_signature.input_specs and hence non_user_input_idx=0 - # The input_specs should be of the form [params, buffers, constant_tensors, user_inputs] + # The input_specs should be of the form [params, buffers, constant_tensors, custom_obj, user_inputs] non_user_input_idx = 0 for node in gm.graph.nodes: if node.op == "get_attr": - if node.target not in state_dict: - raise ValueError( - f"The get_attr node : {node.name} with target: {node.target} value could not be found in state_dict. Please check the input exported_program's graphmodule parameters." - ) - constant_tensor = state_dict[node.target] - input_kind = InputKind.CONSTANT_TENSOR + lift_val = None + input_kind = None - # state_dict has these parameters/buffers as torch.Tensors. We override them as torch.nn.Parameter/torch.Tensors respectively. - for name, _ in gm.named_parameters(): - if node.target == name: - input_kind = InputKind.PARAMETER - state_dict[name] = torch.nn.Parameter(state_dict[name]) - break - for name, _ in gm.named_buffers(): - if node.target == name: - input_kind = InputKind.BUFFER - break + if node.target not in state_dict: + constants[node.target] = getattr(gm, node.target) + input_kind = InputKind.CUSTOM_OBJ + lift_val = constants[node.target] + else: + lift_val = state_dict[node.target] + + input_kind = InputKind.CONSTANT_TENSOR + + # state_dict has these parameters/buffers as torch.Tensors. We override them as torch.nn.Parameter/torch.Tensors respectively. + for name, _ in gm.named_parameters(): + if node.target == name: + input_kind = InputKind.PARAMETER + state_dict[name] = torch.nn.Parameter(state_dict[name]) + break + for name, _ in gm.named_buffers(): + if node.target == name: + input_kind = InputKind.BUFFER + break + + assert lift_val is not None and input_kind is not None # Replace get_attr nodes with placeholder nodes and copy metadata. with gm.graph.inserting_before(first_user_input): - const_placeholder_node = gm.graph.placeholder(node.target) + const_placeholder_node = gm.graph.placeholder( + node.target.replace(".", "_") + ) # Copy the node meta into this new placeholder node const_placeholder_node.meta = node.meta - const_placeholder_node.meta["val"] = cast( - FakeTensor, - torch.empty_strided( - tuple(constant_tensor.shape), - tuple([1] * len(constant_tensor.shape)), - ), - ) + + if isinstance(lift_val, torch.Tensor): + const_placeholder_node.meta["val"] = cast( + FakeTensor, + torch.empty_strided( + tuple(lift_val.shape), + tuple([1] * len(lift_val.shape)), + ), + ) node.replace_all_uses_with(const_placeholder_node) gm.graph.erase_node(node) # Add these parameters/buffers/constants to the existing graph signature # before user inputs. These specs are looked up in the state_dict during ExportedProgram creation. + input_spec_arg = TensorArgument(name=const_placeholder_node.name) + if input_kind == InputKind.CUSTOM_OBJ: + input_spec_arg = CustomObjArgument( + name=const_placeholder_node.name, class_fqn="" + ) graph_signature.input_specs.insert( non_user_input_idx, InputSpec( kind=input_kind, - arg=TensorArgument(name=const_placeholder_node.name), + arg=input_spec_arg, target=node.target, ), ) @@ -143,7 +167,7 @@ def lift(gm: torch.fx.GraphModule, graph_signature: Any) -> torch.fx.GraphModule gm.graph.eliminate_dead_code() gm.graph.lint() - return gm, graph_signature, state_dict + return gm, graph_signature, state_dict, constants def get_duplicate_nodes( @@ -281,18 +305,30 @@ def create_trt_exp_program( input_specs=input_specs, output_specs=output_specs ) + module_call_graph = [ + ModuleCallEntry( + "", + ModuleCallSignature( + inputs=[], + outputs=[], + in_spec=gm.graph._codegen.pytree_info.in_spec, + out_spec=gm.graph._codegen.pytree_info.out_spec, + ), + ) + ] + # Lift parameters/buffers/constants in the graph # torch.export serialization expects them to be lifted - gm, trt_graph_signature, state_dict = lift(gm, trt_graph_signature) + gm, trt_graph_signature, state_dict, constants = lift(gm, trt_graph_signature) trt_exp_program = ExportedProgram( - gm, - gm.graph, - trt_graph_signature, - state_dict, - {}, - [], - [], + root=gm, + graph=gm.graph, + graph_signature=trt_graph_signature, + state_dict=state_dict, + range_constraints={}, + module_call_graph=module_call_graph, + constants=constants, ) return trt_exp_program @@ -319,9 +355,13 @@ def inline_trt_modules( num_outputs = len(outputs_map[trt_module_node.name]) # Insert a call_function node to perform inference on TRT engine with gm.graph.inserting_before(trt_module_node): + engine_name = f"{name}_engine" + setattr(gm, engine_name, trt_module.engine) + engine_node = gm.graph.get_attr(engine_name) + trt_node = gm.graph.call_function( torch.ops.tensorrt.execute_engine.default, - (trt_module_node.args, trt_module.engine), + (trt_module_node.args, engine_node), ) trt_node.meta["val"] = [] assert num_outputs > 0 @@ -337,6 +377,13 @@ def inline_trt_modules( ) ) + # meta["val"] should be a lighter version of a tensor. For eg: it should be a FakeTensor (with output shape and dtype properties) + # Lighter version of a custom_obj is not defined clearly. meta["val"] does not have any type expectations but + # for custom object nodes, it should be CustomObjArgument + engine_node.meta["val"] = CustomObjArgument( + name=engine_node.name, class_fqn="" + ) + if num_outputs == 1: # Insert getitem nodes as outputs (for export serialization to work) with gm.graph.inserting_after(trt_node): diff --git a/tests/py/dynamo/models/test_export_serde.py b/tests/py/dynamo/models/test_export_serde.py index 0905c5e859..40fa01c2c9 100644 --- a/tests/py/dynamo/models/test_export_serde.py +++ b/tests/py/dynamo/models/test_export_serde.py @@ -146,7 +146,6 @@ def forward(self, x): ) ], "ir": ir, - "debug": True, } exp_program = torchtrt.dynamo.trace(model, **compile_spec) @@ -306,6 +305,7 @@ def forward(self, x): exp_program = torchtrt.dynamo.trace(model, **compile_spec) trt_module = torchtrt.dynamo.compile(exp_program, **compile_spec) + torchtrt.save(trt_module, "/tmp/trt.ep", inputs=[input]) deser_trt_exp_program = torch.export.load("/tmp/trt.ep") deser_trt_module = deser_trt_exp_program.module() From b191d62bafac9740657cb9dc67ccafb213d4914c Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 18 Mar 2024 16:29:04 -0700 Subject: [PATCH 007/122] chore: update to 2.3 rc build --- py/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/py/requirements.txt b/py/requirements.txt index cd52d32436..419c325653 100644 --- a/py/requirements.txt +++ b/py/requirements.txt @@ -1,9 +1,9 @@ numpy packaging pybind11==2.6.2 ---extra-index-url https://download.pytorch.org/whl/nightly/cu121 -torch>=2.3.0.dev,<2.4.0 -torchvision>=0.18.0.dev,<0.19.0 +--index-url https://download.pytorch.org/whl/test/cu121 +torch>=2.3.0,<2.4.0 +torchvision>=0.18.0,<0.19.0 --extra-index-url https://pypi.ngc.nvidia.com tensorrt==8.6.1 pyyaml From 8674a3c437d767c6e2b09db3a574af9a99c318d3 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 19 Mar 2024 16:18:17 -0700 Subject: [PATCH 008/122] chore: minor fixes --- py/torch_tensorrt/dynamo/_exporter.py | 1 + .../lowering/test_aten_lowering_passes.py | 12 ++++++++---- tests/py/dynamo/models/test_models_export.py | 19 ++++++++++--------- tests/py/dynamo/testing_utilities.py | 1 + 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/py/torch_tensorrt/dynamo/_exporter.py b/py/torch_tensorrt/dynamo/_exporter.py index cf06bc4531..d4a9fd3584 100644 --- a/py/torch_tensorrt/dynamo/_exporter.py +++ b/py/torch_tensorrt/dynamo/_exporter.py @@ -129,6 +129,7 @@ def lift( # Replace get_attr nodes with placeholder nodes and copy metadata. with gm.graph.inserting_before(first_user_input): + # Ensure name doesn't contain period as it is used for submodules const_placeholder_node = gm.graph.placeholder( node.target.replace(".", "_") ) diff --git a/tests/py/dynamo/lowering/test_aten_lowering_passes.py b/tests/py/dynamo/lowering/test_aten_lowering_passes.py index bc75a8aa3d..3afc5e5923 100644 --- a/tests/py/dynamo/lowering/test_aten_lowering_passes.py +++ b/tests/py/dynamo/lowering/test_aten_lowering_passes.py @@ -1,9 +1,12 @@ import torch -from torch.testing._internal.common_utils import TestCase, run_tests - import torch_tensorrt +from torch.testing._internal.common_utils import TestCase, run_tests -from ..testing_utilities import DECIMALS_OF_AGREEMENT, lower_graph_testing +from ..testing_utilities import ( + DECIMALS_OF_AGREEMENT, + DECIMALS_OF_AGREEMENT_3, + lower_graph_testing, +) class TestInputAsOutput(TestCase): @@ -444,10 +447,11 @@ def forward(self, input, weight, bias): max_diff = float( torch.max(torch.abs(optimized_model_results - torch_model_results)) ) + self.assertAlmostEqual( max_diff, 0, - DECIMALS_OF_AGREEMENT, + DECIMALS_OF_AGREEMENT_3, msg=f"Linear TRT outputs don't match with the original model.", ) torch._dynamo.reset() diff --git a/tests/py/dynamo/models/test_models_export.py b/tests/py/dynamo/models/test_models_export.py index fd7b40592a..bc8bf12c95 100644 --- a/tests/py/dynamo/models/test_models_export.py +++ b/tests/py/dynamo/models/test_models_export.py @@ -159,11 +159,11 @@ def test_bert_base_uncased(ir): model = BertModel.from_pretrained("bert-base-uncased").cuda().eval() input = torch.randint(0, 1, (1, 14), dtype=torch.int32).to("cuda") input2 = torch.randint(0, 1, (1, 14), dtype=torch.int32).to("cuda") - model = ( - transformers_trace(model, input_names=["input_ids", "attention_mask"]) - .eval() - .cuda() - ) + # model = ( + # transformers_trace(model, input_names=["input_ids", "attention_mask"]) + # .eval() + # .cuda() + # ) compile_spec = { "inputs": [ @@ -182,8 +182,8 @@ def test_bert_base_uncased(ir): "enabled_precisions": {torch.float}, "truncate_long_and_double": True, "ir": ir, - "min_block_size": 10, - "torch_executed_ops": {"torch.ops.aten.gelu.default"}, + "min_block_size": 15, + "debug": True, } trt_mod = torchtrt.compile(model, **compile_spec) model_outputs = model(input, input2) @@ -192,8 +192,9 @@ def test_bert_base_uncased(ir): len(model_outputs) == len(trt_model_outputs), msg=f"Number of outputs for BERT model compilation is different with Pytorch {len(model_outputs)} and TensorRT {len(trt_model_outputs)}. Please check the compilation.", ) - for index, key in enumerate(model_outputs): - out, trt_out = model_outputs[key], trt_model_outputs[index] + + for key, _ in model_outputs.items(): + out, trt_out = model_outputs[key], trt_model_outputs[key] cos_sim = cosine_similarity(out, trt_out) assertions.assertTrue( cos_sim > COSINE_THRESHOLD, diff --git a/tests/py/dynamo/testing_utilities.py b/tests/py/dynamo/testing_utilities.py index 742b9fc1a3..c815d2fde4 100644 --- a/tests/py/dynamo/testing_utilities.py +++ b/tests/py/dynamo/testing_utilities.py @@ -14,6 +14,7 @@ ) DECIMALS_OF_AGREEMENT = 4 +DECIMALS_OF_AGREEMENT_3 = 3 def fx_dynamo_testing_backend( From f4e8fe9bc3f114dd8da3760dca510f78a8f58a0d Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 19 Mar 2024 17:04:38 -0700 Subject: [PATCH 009/122] chore: remove duplicate bert test case --- tests/py/dynamo/models/test_models_export.py | 53 -------------------- 1 file changed, 53 deletions(-) diff --git a/tests/py/dynamo/models/test_models_export.py b/tests/py/dynamo/models/test_models_export.py index bc8bf12c95..4d0f4e2e7f 100644 --- a/tests/py/dynamo/models/test_models_export.py +++ b/tests/py/dynamo/models/test_models_export.py @@ -105,55 +105,6 @@ def test_efficientnet_b0(ir): torch._dynamo.reset() -@pytest.mark.unit -def test_bert_base_uncased(ir): - model = BertModel.from_pretrained("bert-base-uncased").cuda().eval() - input = torch.randint(0, 1, (1, 14), dtype=torch.int32).to("cuda") - input2 = torch.randint(0, 1, (1, 14), dtype=torch.int32).to("cuda") - model = ( - transformers_trace(model, input_names=["input_ids", "attention_mask"]) - .eval() - .cuda() - ) - - compile_spec = { - "inputs": [ - torchtrt.Input( - input.shape, - dtype=input.dtype, - format=torch.contiguous_format, - ), - torchtrt.Input( - input.shape, - dtype=input.dtype, - format=torch.contiguous_format, - ), - ], - "device": torchtrt.Device("cuda:0"), - "enabled_precisions": {torch.float}, - "truncate_long_and_double": True, - "ir": ir, - "min_block_size": 10, - } - trt_mod = torchtrt.compile(model, **compile_spec) - model_outputs = model(input, input2) - trt_model_outputs = trt_mod(input, input2) - assertions.assertTrue( - len(model_outputs) == len(trt_model_outputs), - msg=f"Number of outputs for BERT model compilation is different with Pytorch {len(model_outputs)} and TensorRT {len(trt_model_outputs)}. Please check the compilation.", - ) - for index, key in enumerate(model_outputs): - out, trt_out = model_outputs[key], trt_model_outputs[index] - cos_sim = cosine_similarity(out, trt_out) - assertions.assertTrue( - cos_sim > COSINE_THRESHOLD, - msg=f"HF BERT base-uncased TRT outputs don't match with the original model. Cosine sim score: {cos_sim} Threshold: {COSINE_THRESHOLD}", - ) - - # Clean up model env - torch._dynamo.reset() - - @pytest.mark.unit def test_bert_base_uncased(ir): model = BertModel.from_pretrained("bert-base-uncased").cuda().eval() @@ -183,7 +134,6 @@ def test_bert_base_uncased(ir): "truncate_long_and_double": True, "ir": ir, "min_block_size": 15, - "debug": True, } trt_mod = torchtrt.compile(model, **compile_spec) model_outputs = model(input, input2) @@ -204,9 +154,6 @@ def test_bert_base_uncased(ir): # Clean up model env torch._dynamo.reset() - with torch.no_grad(): - torch.cuda.empty_cache() - @pytest.mark.unit def test_resnet18_half(ir): From 4ae6ab95a98f5b1f644c3bfd3824ac76df442dc7 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 19 Mar 2024 17:05:51 -0700 Subject: [PATCH 010/122] chore: remove comments --- tests/py/dynamo/models/test_models_export.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/py/dynamo/models/test_models_export.py b/tests/py/dynamo/models/test_models_export.py index 4d0f4e2e7f..84f6bf7a36 100644 --- a/tests/py/dynamo/models/test_models_export.py +++ b/tests/py/dynamo/models/test_models_export.py @@ -110,11 +110,6 @@ def test_bert_base_uncased(ir): model = BertModel.from_pretrained("bert-base-uncased").cuda().eval() input = torch.randint(0, 1, (1, 14), dtype=torch.int32).to("cuda") input2 = torch.randint(0, 1, (1, 14), dtype=torch.int32).to("cuda") - # model = ( - # transformers_trace(model, input_names=["input_ids", "attention_mask"]) - # .eval() - # .cuda() - # ) compile_spec = { "inputs": [ From fff1b800f970f8ae56f9d7e48189095d57840a74 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 12 Mar 2024 02:11:29 -0700 Subject: [PATCH 011/122] chore: Upgrade to TRT 10.0 chore: updates to trt api chore: trt 10 fixes chore: more fixes --- .../converters/impl/constant_pad.cpp | 2 +- .../converters/impl/conv_deconv.cpp | 6 +- .../converters/impl/interpolate.cpp | 50 +++--- core/conversion/converters/impl/linear.cpp | 30 ++-- core/runtime/TRTEngine.cpp | 47 ++++-- cpp/include/torch_tensorrt/ptq.h | 8 +- py/torch_tensorrt/csrc/torch_tensorrt_py.cpp | 5 + py/torch_tensorrt/dynamo/_compiler.py | 2 +- .../dynamo/conversion/_TRTInterpreter.py | 6 +- .../dynamo/conversion/_conversion.py | 3 +- .../dynamo/conversion/impl/conv.py | 2 +- .../dynamo/conversion/impl/deconv.py | 2 +- .../conversion/impl/elementwise/base.py | 11 -- .../dynamo/conversion/impl/pad.py | 8 +- .../dynamo/conversion/impl/permutation.py | 4 +- .../dynamo/conversion/impl/upsample.py | 6 +- .../runtime/_PythonTorchTensorRTModule.py | 142 +++++++++--------- .../fx/converters/acc_ops_converters.py | 27 ++-- py/torch_tensorrt/fx/utils.py | 18 ++- .../test_convert_method_to_trt_engine.py | 8 +- 20 files changed, 205 insertions(+), 182 deletions(-) diff --git a/core/conversion/converters/impl/constant_pad.cpp b/core/conversion/converters/impl/constant_pad.cpp index 4191cb1bab..20a091e730 100644 --- a/core/conversion/converters/impl/constant_pad.cpp +++ b/core/conversion/converters/impl/constant_pad.cpp @@ -55,7 +55,7 @@ auto constant_pad_registrations TORCHTRT_UNUSED = RegisterNodeConversionPatterns util::toDims(c10::IntArrayRef(stride))); TORCHTRT_CHECK(slice_layer, "Unable to create slice layer from node: " << *n); slice_layer->setName((util::node_info(n) + "_slice").c_str()); - slice_layer->setMode(nvinfer1::SliceMode::kFILL); + slice_layer->setMode(nvinfer1::SampleMode::kFILL); slice_layer->setInput(4, *value_itensor); if (ctx->input_is_dynamic) { diff --git a/core/conversion/converters/impl/conv_deconv.cpp b/core/conversion/converters/impl/conv_deconv.cpp index 66620197a9..bf0df00c64 100644 --- a/core/conversion/converters/impl/conv_deconv.cpp +++ b/core/conversion/converters/impl/conv_deconv.cpp @@ -61,7 +61,7 @@ nvinfer1::ILayer* add_bias_layer( auto* sliceLayer = ctx->net->addSlice(*input_tensor, dummy, dummy, stride); sliceLayer->setInput(1, *start); sliceLayer->setInput(2, *size); - sliceLayer->setMode(nvinfer1::SliceMode::kFILL); + sliceLayer->setMode(nvinfer1::SampleMode::kFILL); nvinfer1::ITensor* slice_output = sliceLayer->getOutput(0); nvinfer1::Dims constantDims; @@ -194,7 +194,7 @@ bool add_conv_deconv(ConversionCtx* ctx, const torch::jit::Node* n, args& args) nvinfer1::IConvolutionLayer* convLayer = ctx->net->addConvolutionNd(*in, num_output_maps, filter_dim, kernel_weights, bias.data); convLayer->setStrideNd(stride); - convLayer->setPaddingMode(nvinfer1::PaddingMode::kCAFFE_ROUND_DOWN); + convLayer->setPaddingMode(nvinfer1::PaddingMode::kEXPLICIT_ROUND_DOWN); convLayer->setPaddingNd(padding); convLayer->setPostPadding(out_padding); convLayer->setDilationNd(dilation); @@ -293,7 +293,7 @@ bool add_conv_deconv(ConversionCtx* ctx, const torch::jit::Node* n, args& args) TORCHTRT_CHECK(conv, "Unable to create convolution layer from node: " << *n); conv->setStrideNd(stride); - conv->setPaddingMode(nvinfer1::PaddingMode::kCAFFE_ROUND_DOWN); + conv->setPaddingMode(nvinfer1::PaddingMode::kEXPLICIT_ROUND_DOWN); conv->setPaddingNd(padding); conv->setPostPadding(out_padding); conv->setDilationNd(dilation); diff --git a/core/conversion/converters/impl/interpolate.cpp b/core/conversion/converters/impl/interpolate.cpp index fad2ca5121..61721e3d2c 100644 --- a/core/conversion/converters/impl/interpolate.cpp +++ b/core/conversion/converters/impl/interpolate.cpp @@ -72,7 +72,7 @@ void resize_layer_size( nvinfer1::ITensor* in, std::vector out_shape, std::vector scales, - nvinfer1::ResizeMode mode, + nvinfer1::InterpolationMode mode, bool align_corners = false) { TORCHTRT_CHECK((out_shape.size() > 0) ^ (scales.size() > 0), "only one of out_shape or scales should be defined"); auto resize_layer = ctx->net->addResize(*in); @@ -141,7 +141,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = float scale = args[2].IValue()->toDouble(); std::vector padded_scales(in_shape.size(), 1); padded_scales[padded_scales.size() - 1] = scale; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kNEAREST); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -150,7 +150,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kNEAREST); } return true; @@ -172,7 +172,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = float scale = scale_factors[0]; std::vector padded_scales(in_shape.size(), 1); padded_scales[padded_scales.size() - 1] = scale; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kNEAREST); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -181,7 +181,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kNEAREST); } return true; @@ -203,7 +203,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = std::vector padded_scales(in_shape.size(), 1); padded_scales[padded_scales.size() - 2] = scale_h; padded_scales[padded_scales.size() - 1] = scale_w; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kNEAREST); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -212,7 +212,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kNEAREST); } return true; @@ -236,7 +236,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = std::vector padded_scales(in_shape.size(), 1); padded_scales[padded_scales.size() - 2] = scale_h; padded_scales[padded_scales.size() - 1] = scale_w; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kNEAREST); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -245,7 +245,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kNEAREST); } return true; @@ -270,7 +270,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = padded_scales[padded_scales.size() - 3] = scale_d; padded_scales[padded_scales.size() - 2] = scale_h; padded_scales[padded_scales.size() - 1] = scale_w; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kNEAREST); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -279,7 +279,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kNEAREST); } return true; @@ -306,7 +306,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = padded_scales[padded_scales.size() - 3] = scale_d; padded_scales[padded_scales.size() - 2] = scale_h; padded_scales[padded_scales.size() - 1] = scale_w; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kNEAREST); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -315,7 +315,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kNEAREST); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kNEAREST); } return true; @@ -336,7 +336,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = float scale = args[3].IValue()->toDouble(); std::vector padded_scales(in_shape.size(), 1); padded_scales[padded_scales.size() - 1] = scale; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kLINEAR, align_corners); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -345,7 +345,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kLINEAR, align_corners); } return true; @@ -368,7 +368,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = float scale = scale_factors[0]; std::vector padded_scales(in_shape.size(), 1); padded_scales[padded_scales.size() - 1] = scale; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kLINEAR, align_corners); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -377,7 +377,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kLINEAR, align_corners); } return true; @@ -400,7 +400,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = std::vector padded_scales(in_shape.size(), 1); padded_scales[padded_scales.size() - 2] = scale_h; padded_scales[padded_scales.size() - 1] = scale_w; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kLINEAR, align_corners); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -410,7 +410,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kLINEAR, align_corners); } return true; @@ -435,7 +435,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = std::vector padded_scales(in_shape.size(), 1); padded_scales[padded_scales.size() - 2] = scale_h; padded_scales[padded_scales.size() - 1] = scale_w; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kLINEAR, align_corners); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -445,7 +445,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kLINEAR, align_corners); } return true; @@ -470,7 +470,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = padded_scales[padded_scales.size() - 3] = scale_d; padded_scales[padded_scales.size() - 2] = scale_h; padded_scales[padded_scales.size() - 1] = scale_w; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kLINEAR, align_corners); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -480,7 +480,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kLINEAR, align_corners); } return true; @@ -507,7 +507,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = padded_scales[padded_scales.size() - 3] = scale_d; padded_scales[padded_scales.size() - 2] = scale_h; padded_scales[padded_scales.size() - 1] = scale_w; - resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, {}, padded_scales, nvinfer1::InterpolationMode::kLINEAR, align_corners); } else { // Case 2: user uses output size auto out_size = util::toVec(util::toDims(args[1].unwrapToIntList())); @@ -517,7 +517,7 @@ auto interpolate_registrations TORCHTRT_UNUSED = auto out_shape = in_shape; std::copy(out_size.begin(), out_size.end(), out_shape.begin() + (in_shape.size() - out_size.size())); - resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::ResizeMode::kLINEAR, align_corners); + resize_layer_size(ctx, n, in, out_shape, {}, nvinfer1::InterpolationMode::kLINEAR, align_corners); } return true; diff --git a/core/conversion/converters/impl/linear.cpp b/core/conversion/converters/impl/linear.cpp index 6289334736..39f9cb055d 100644 --- a/core/conversion/converters/impl/linear.cpp +++ b/core/conversion/converters/impl/linear.cpp @@ -40,22 +40,26 @@ auto linear_registrations TORCHTRT_UNUSED = RegisterNodeConversionPatterns().pat in = in_shuffle->getOutput(0); } - auto w_tensor = args[1].IValue()->toTensor(); - Weights w = Weights(ctx, w_tensor); + // Convert w_tensor to ITensor + auto weight = args[1].IValue()->toTensor(); + auto weight_tensor = tensor_to_const(ctx, weight, util::node_info(n) + "_weight"); + auto mm_layer = ctx->net->addMatrixMultiply( + *in, nvinfer1::MatrixOperation::kNONE, *weight_tensor, nvinfer1::MatrixOperation::kNONE); - nvinfer1::ILayer* new_layer; - if (!args[2].IValue()->isNone()) { - Weights b(ctx, args[2].IValue()->toTensor()); - new_layer = ctx->net->addFullyConnected(*in, w.num_output_maps, w.data, b.data); - } else { - LOG_DEBUG("There is no bias for the linear layer"); - new_layer = ctx->net->addFullyConnected(*in, w.num_output_maps, w.data, Weights().data); - } + TORCHTRT_CHECK(mm_layer, "Unable to create linear layer from node: " << *n); + mm_layer->setName(util::node_info(n).c_str()); - TORCHTRT_CHECK(new_layer, "Unable to create linear layer from node: " << *n); + auto mm_output = mm_layer->getOutput(0); - new_layer->setName(util::node_info(n).c_str()); - auto out_tensor = ctx->AssociateValueAndTensor(n->outputs()[0], new_layer->getOutput(0)); + if (!args[2].IValue()->isNone()) { + // Convert bias to ITensor + auto bias = args[2].IValue()->toTensor(); + auto bias_tensor = tensor_to_const(ctx, bias, util::node_info(n) + "_bias"); + auto bias_add_layer = add_elementwise( + ctx, nvinfer1::ElementWiseOperation::kSUM, mm_output, bias_tensor, util::node_info(n) + "_bias_add"); + mm_output = bias_add_layer->getOutput(0); + } + auto out_tensor = ctx->AssociateValueAndTensor(n->outputs()[0], mm_output); LOG_DEBUG("Output tensor shape: " << out_tensor->getDimensions()); diff --git a/core/runtime/TRTEngine.cpp b/core/runtime/TRTEngine.cpp index 7a046f6d94..fcbb403e2c 100644 --- a/core/runtime/TRTEngine.cpp +++ b/core/runtime/TRTEngine.cpp @@ -120,16 +120,25 @@ TRTEngine::TRTEngine( } else { uint64_t inputs_size = _in_binding_names.size(); in_binding_names.resize(inputs_size); - for (size_t pyt_idx = 0; pyt_idx < inputs_size; pyt_idx++) { + for (uint64_t pyt_idx = 0; pyt_idx < inputs_size; pyt_idx++) { auto binding_name = _in_binding_names[pyt_idx]; - auto trt_idx = cuda_engine->getBindingIndex(binding_name.c_str()); - std::string engine_binded_name = cuda_engine->getIOTensorName(trt_idx); - TORCHTRT_CHECK( - (binding_name == engine_binded_name), - "Could not find a TensorRT engine binding for input named " << binding_name); + // Check if the binding name provided is in the list of engine's bindings + // by iterating through nbIOTensors and verify it is an input binding + bool is_binding = false, is_input = false; + int32_t trt_idx; + for (int32_t idx = 0; idx < cuda_engine->getNbIOTensors(); idx++) { + std::string curr_bind_name = cuda_engine->getIOTensorName(idx); + if (curr_bind_name == binding_name) { + is_binding = true; + trt_idx = idx; + if (cuda_engine->getTensorIOMode(binding_name.c_str()) == nvinfer1::TensorIOMode::kINPUT) { + is_input = true; + } + } + } + TORCHTRT_CHECK(is_binding, "Could not find a TensorRT engine binding for input named " << binding_name); TORCHTRT_CHECK( - (cuda_engine->getTensorIOMode(binding_name.c_str()) == nvinfer1::TensorIOMode::kINPUT), - "Binding " << binding_name << " specified as input but found as output in TensorRT engine"); + is_input, "Binding " << binding_name << " specified as input but found as output in TensorRT engine"); LOG_DEBUG( "Input binding name: " << binding_name << " has TensorRT binding index: " << trt_idx << ", Torch binding index: " << pyt_idx); @@ -141,11 +150,25 @@ TRTEngine::TRTEngine( out_binding_names.resize(outputs); for (size_t pyt_idx = 0; pyt_idx < outputs; pyt_idx++) { auto binding_name = _out_binding_names[pyt_idx]; - auto trt_idx = cuda_engine->getBindingIndex(binding_name.c_str()); - TORCHTRT_CHECK((trt_idx != -1), "Could not find a TensorRT engine binding for output named " << binding_name); + // Check if the binding name provided is in the list of engine's bindings + // by iterating through nbIOTensors and verify it is an output binding + bool is_binding = false, is_output = false; + int32_t trt_idx; + for (int32_t idx = 0; idx < cuda_engine->getNbIOTensors(); idx++) { + std::string curr_bind_name = cuda_engine->getIOTensorName(idx); + if (curr_bind_name == binding_name) { + is_binding = true; + trt_idx = idx; + if (cuda_engine->getTensorIOMode(binding_name.c_str()) == nvinfer1::TensorIOMode::kOUTPUT) { + is_output = true; + } + } + } + + TORCHTRT_CHECK(is_binding, "Could not find a TensorRT engine binding for output named " << binding_name); TORCHTRT_CHECK( - !(cuda_engine->getTensorIOMode(binding_name.c_str()) == nvinfer1::TensorIOMode::kINPUT), - "Binding " << binding_name << " specified as output but found as input in TensorRT engine"); + is_output, "Binding " << binding_name << " specified as output but found as input in TensorRT engine"); + LOG_DEBUG( "Output binding name: " << binding_name << " has TensorRT binding index: " << trt_idx << ", Torch binding index: " << inputs_size + pyt_idx); diff --git a/cpp/include/torch_tensorrt/ptq.h b/cpp/include/torch_tensorrt/ptq.h index d8570f0e6e..e88da06b0c 100644 --- a/cpp/include/torch_tensorrt/ptq.h +++ b/cpp/include/torch_tensorrt/ptq.h @@ -21,10 +21,10 @@ #include "torch_tensorrt/macros.h" #ifndef DOXYGEN_SHOULD_SKIP_THIS -namespace nvinfer1 { -class IInt8Calibrator; -class IInt8EntropyCalibrator2; -} // namespace nvinfer1 +// namespace nvinfer1 { +// class IInt8Calibrator; +// class IInt8EntropyCalibrator2; +// } // namespace nvinfer1 namespace torch_tensorrt { namespace ptq { diff --git a/py/torch_tensorrt/csrc/torch_tensorrt_py.cpp b/py/torch_tensorrt/csrc/torch_tensorrt_py.cpp index 33c7e27398..c0655d9907 100644 --- a/py/torch_tensorrt/csrc/torch_tensorrt_py.cpp +++ b/py/torch_tensorrt/csrc/torch_tensorrt_py.cpp @@ -2,6 +2,7 @@ #include "pybind11/stl.h" #include "ATen/core/jit_type.h" +#include "NvInferRuntimeBase.h" #include "Python.h" #include "core/compiler.h" #include "core/conversion/conversion.h" @@ -77,6 +78,10 @@ class pyIInt8Calibrator : public pyCalibratorTrampoline; using Derived::Derived; + nvinfer1::InterfaceInfo getInterfaceInfo() const noexcept override { + return nvinfer1::InterfaceInfo{"PYTHON CALIBRATOR", 1, 0}; + } + nvinfer1::CalibrationAlgoType getAlgorithm() noexcept override { try { PYBIND11_OVERLOAD_PURE_NAME( diff --git a/py/torch_tensorrt/dynamo/_compiler.py b/py/torch_tensorrt/dynamo/_compiler.py index b321eabcb2..ac430bf883 100644 --- a/py/torch_tensorrt/dynamo/_compiler.py +++ b/py/torch_tensorrt/dynamo/_compiler.py @@ -634,7 +634,7 @@ def convert_module_to_trt_engine( import io with io.BytesIO() as engine_bytes: - engine_bytes.write(interpreter_result.engine.serialize()) + engine_bytes.write(interpreter_result.engine) engine_bytearray = engine_bytes.getvalue() return engine_bytearray diff --git a/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py b/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py index 06ae596ed0..ffcc9c195e 100644 --- a/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py +++ b/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py @@ -172,7 +172,7 @@ def run( if version.parse(trt.__version__) >= version.parse("8.2"): builder_config.profiling_verbosity = ( - trt.ProfilingVerbosity.VERBOSE + trt.ProfilingVerbosity.DETAILED if self.compilation_settings.debug else trt.ProfilingVerbosity.LAYER_NAMES_ONLY ) @@ -252,7 +252,7 @@ def run( if tactic_sources is not None: builder_config.set_tactic_sources(tactic_sources=tactic_sources) - engine = self.builder.build_engine(self.ctx.net, builder_config) + engine = self.builder.build_serialized_network(self.ctx.net, builder_config) assert engine serialized_cache = ( @@ -263,7 +263,7 @@ def run( _LOGGER.info( f"Build TRT engine elapsed time: {datetime.now() - build_engine_start_time}" ) - _LOGGER.info(f"TRT Engine uses: {engine.device_memory_size} bytes of Memory") + _LOGGER.info(f"TRT Engine uses: {engine.nbytes} bytes of Memory") return TRTInterpreterResult( engine, self._input_names, self._output_names, serialized_cache diff --git a/py/torch_tensorrt/dynamo/conversion/_conversion.py b/py/torch_tensorrt/dynamo/conversion/_conversion.py index 844cb6789a..f1f4af9236 100644 --- a/py/torch_tensorrt/dynamo/conversion/_conversion.py +++ b/py/torch_tensorrt/dynamo/conversion/_conversion.py @@ -87,8 +87,9 @@ def convert_module( from torch_tensorrt.dynamo.runtime import TorchTensorRTModule with io.BytesIO() as engine_bytes: - engine_bytes.write(interpreter_result.engine.serialize()) + engine_bytes.write(interpreter_result.engine) engine_str = engine_bytes.getvalue() + return TorchTensorRTModule( serialized_engine=engine_str, name=name, diff --git a/py/torch_tensorrt/dynamo/conversion/impl/conv.py b/py/torch_tensorrt/dynamo/conversion/impl/conv.py index 26e0d59b8f..6c15b4b5fe 100644 --- a/py/torch_tensorrt/dynamo/conversion/impl/conv.py +++ b/py/torch_tensorrt/dynamo/conversion/impl/conv.py @@ -63,7 +63,7 @@ def convNd( ) # Process weight terms - if ctx.net.has_explicit_precision or isinstance(weight, TRTTensor): + if isinstance(weight, TRTTensor): weight = get_trt_tensor(ctx, weight, f"{name}_weight") # Append new dimension (unsqueeze) if the convolution is 1d if is_conv1d: diff --git a/py/torch_tensorrt/dynamo/conversion/impl/deconv.py b/py/torch_tensorrt/dynamo/conversion/impl/deconv.py index f66bff7c82..03a209e2a5 100644 --- a/py/torch_tensorrt/dynamo/conversion/impl/deconv.py +++ b/py/torch_tensorrt/dynamo/conversion/impl/deconv.py @@ -63,7 +63,7 @@ def deconvNd( ) # Process weight terms - if ctx.net.has_explicit_precision or isinstance(weight, TRTTensor): + if isinstance(weight, TRTTensor): weight = get_trt_tensor(ctx, weight, f"{name}_weight") # Append new dimension (unsqueeze) if the deconvolution is 1d if is_deconv1d: diff --git a/py/torch_tensorrt/dynamo/conversion/impl/elementwise/base.py b/py/torch_tensorrt/dynamo/conversion/impl/elementwise/base.py index 8282ee8698..6664a67cfe 100644 --- a/py/torch_tensorrt/dynamo/conversion/impl/elementwise/base.py +++ b/py/torch_tensorrt/dynamo/conversion/impl/elementwise/base.py @@ -147,17 +147,6 @@ def convert_binary_elementwise( ctx, rhs_val, trt_promoted_type, name, target, source_ir ) - # Check the limitation in the doc string. - if ctx.net.has_implicit_batch_dimension: - if is_lhs_trt_tensor and not is_rhs_trt_tensor: - assert len(lhs_val.shape) >= len( - rhs_val.shape - ), f"{lhs_val.shape} >= {rhs_val.shape}" - elif not is_lhs_trt_tensor and is_rhs_trt_tensor: - assert len(rhs_val.shape) >= len( - lhs_val.shape - ), f"{rhs_val.shape} >= {lhs_val.shape}" - lhs_val, rhs_val = broadcast( ctx.net, lhs_val, rhs_val, f"{name}_lhs", f"{name}_rhs" ) diff --git a/py/torch_tensorrt/dynamo/conversion/impl/pad.py b/py/torch_tensorrt/dynamo/conversion/impl/pad.py index 3764667ffb..9031426c5c 100644 --- a/py/torch_tensorrt/dynamo/conversion/impl/pad.py +++ b/py/torch_tensorrt/dynamo/conversion/impl/pad.py @@ -53,7 +53,7 @@ def constant_padNd( ) value_const = get_trt_tensor(ctx, value, f"{name}_value", input.dtype) layer.set_input(4, value_const) - layer.mode = trt.SliceMode.FILL + layer.mode = trt.SampleMode.FILL set_layer_name(layer, target, name, source_ir) return layer.get_output(0) @@ -91,7 +91,7 @@ def reflection_padNd( shape=tuple(new_shape), stride=tuple(stride_list), ) - layer.mode = trt.SliceMode.REFLECT + layer.mode = trt.SampleMode.REFLECT set_layer_name(layer, target, name, source_ir) return layer.get_output(0) @@ -129,7 +129,7 @@ def replication_padNd( shape=tuple(new_shape), stride=tuple(stride_list), ) - layer.mode = trt.SliceMode.CLAMP + layer.mode = trt.SampleMode.CLAMP set_layer_name(layer, target, name, source_ir) return layer.get_output(0) @@ -167,7 +167,7 @@ def circular_padNd( shape=tuple(new_shape), stride=tuple(stride_list), ) - layer.mode = trt.SliceMode.WRAP + layer.mode = trt.SampleMode.WRAP set_layer_name(layer, target, name, source_ir) return layer.get_output(0) diff --git a/py/torch_tensorrt/dynamo/conversion/impl/permutation.py b/py/torch_tensorrt/dynamo/conversion/impl/permutation.py index 48a91faa40..4fabebd176 100644 --- a/py/torch_tensorrt/dynamo/conversion/impl/permutation.py +++ b/py/torch_tensorrt/dynamo/conversion/impl/permutation.py @@ -66,7 +66,7 @@ def roll( shape=shape, stride=stride, ) - layer.mode = trt.SliceMode.WRAP + layer.mode = trt.SampleMode.WRAP set_layer_name(layer, target, f"{name}_slice_wrap", source_ir) return layer.get_output(0) @@ -83,7 +83,7 @@ def roll( shape=flatten_shape, stride=stride, ) - layer.mode = trt.SliceMode.WRAP + layer.mode = trt.SampleMode.WRAP set_layer_name(layer, target, f"{name}_slice_wrap", source_ir) output = layer.get_output(0) output = impl.shuffle.reshape( diff --git a/py/torch_tensorrt/dynamo/conversion/impl/upsample.py b/py/torch_tensorrt/dynamo/conversion/impl/upsample.py index 3313730ec3..c61aad4290 100644 --- a/py/torch_tensorrt/dynamo/conversion/impl/upsample.py +++ b/py/torch_tensorrt/dynamo/conversion/impl/upsample.py @@ -29,14 +29,14 @@ def upsample( resize_layer.scales = [1.0, 1.0] + list(scale_factors) else: raise RuntimeError( - f"At least one of out_shape and scale_factors should be specified." + "At least one of out_shape and scale_factors should be specified." ) # interpolate mode if resize_mode == "nearest" or None: - resize_layer.resize_mode = trt.ResizeMode.NEAREST + resize_layer.resize_mode = trt.InterpolationMode.NEAREST elif resize_mode == "bilinear": - resize_layer.resize_mode = trt.ResizeMode.LINEAR + resize_layer.resize_mode = trt.InterpolationMode.LINEAR if align_corners is None or not align_corners: raise RuntimeError( f"Interpolation works differently is align_corners is False for {resize_mode} mode in PyTorch and TensorRT." diff --git a/py/torch_tensorrt/dynamo/runtime/_PythonTorchTensorRTModule.py b/py/torch_tensorrt/dynamo/runtime/_PythonTorchTensorRTModule.py index 3a66ed3716..82f5817b01 100644 --- a/py/torch_tensorrt/dynamo/runtime/_PythonTorchTensorRTModule.py +++ b/py/torch_tensorrt/dynamo/runtime/_PythonTorchTensorRTModule.py @@ -2,7 +2,7 @@ import logging from contextlib import nullcontext -from typing import Any, Dict, List, Optional, Sequence, Tuple +from typing import Any, Dict, List, Optional, Tuple import tensorrt as trt import torch @@ -55,73 +55,69 @@ def __init__( def _initialize(self) -> None: self.initialized = True + logger = trt.Logger() + runtime = trt.Runtime(logger) + self.engine = runtime.deserialize_cuda_engine(self.engine) self.context = self.engine.create_execution_context() # Indices of inputs/outputs in the trt engine bindings, in the order # as they are in the original PyTorch model. - self.input_binding_indices_in_order: Sequence[int] = [ - self.engine.get_binding_index(name) for name in self.input_names - ] - self.output_binding_indices_in_order: Sequence[int] = [ - self.engine.get_binding_index(name) for name in self.output_names - ] - primary_input_outputs = set() - primary_input_outputs.update(self.input_binding_indices_in_order) - primary_input_outputs.update(self.output_binding_indices_in_order) - self.hidden_output_binding_indices_in_order: Sequence[int] = [] - self.hidden_output_names: Sequence[str] = [] - for i in range( - self.engine.num_bindings // self.engine.num_optimization_profiles - ): - if i not in primary_input_outputs: - self.hidden_output_binding_indices_in_order.append(i) - self.hidden_output_names.append(self.engine.get_binding_name(i)) - assert (self.engine.num_bindings // self.engine.num_optimization_profiles) == ( + # TODO: Verify if the following is required especially the hidden outputs + # self.input_binding_indices_in_order: Sequence[int] = [ + # self.engine.get_binding_index(name) for name in self.input_names + # ] + # self.output_binding_indices_in_order: Sequence[int] = [ + # self.engine.get_binding_index(name) for name in self.output_names + # ] + # primary_input_outputs = set() + # primary_input_outputs.update(self.input_binding_indices_in_order) + # primary_input_outputs.update(self.output_binding_indices_in_order) + # self.hidden_output_binding_indices_in_order: Sequence[int] = [] + # self.hidden_output_names: Sequence[str] = [] + # for i in range( + # self.engine.num_bindings // self.engine.num_optimization_profiles + # ): + # if i not in primary_input_outputs: + # self.hidden_output_binding_indices_in_order.append(i) + # self.hidden_output_names.append(self.engine.get_binding_name(i)) + + assert ( + self.engine.num_io_tensors // self.engine.num_optimization_profiles + ) == ( len(self.input_names) + len(self.output_names) - + len(self.hidden_output_names) + # + len(self.hidden_output_names) #TODO: Verify if this is required ) self.input_dtypes = [ unified_dtype_converter( - self.engine.get_binding_dtype(idx), Frameworks.TORCH + self.engine.get_tensor_dtype(input_name), Frameworks.TORCH ) - for idx in self.input_binding_indices_in_order + for input_name in self.input_names ] - self.input_shapes: Sequence[Sequence[int]] = [ - tuple(self.engine.get_binding_shape(idx)) - for idx in self.input_binding_indices_in_order + self.input_shapes = [ + self.engine.get_tensor_shape(input_name) for input_name in self.input_names ] self.output_dtypes = [ unified_dtype_converter( - self.engine.get_binding_dtype(idx), Frameworks.TORCH + self.engine.get_tensor_dtype(output_name), Frameworks.TORCH ) - for idx in self.output_binding_indices_in_order + for output_name in self.output_names ] self.output_shapes = [ - ( - tuple(self.engine.get_binding_shape(idx)) - if self.engine.has_implicit_batch_dimension - else tuple() - ) - for idx in self.output_binding_indices_in_order - ] - self.hidden_output_dtypes = [ - unified_dtype_converter( - self.engine.get_binding_dtype(idx), Frameworks.TORCH - ) - for idx in self.hidden_output_binding_indices_in_order - ] - self.hidden_output_shapes = [ - ( - tuple(self.engine.get_binding_shape(idx)) - if self.engine.has_implicit_batch_dimension - else tuple() - ) - for idx in self.hidden_output_binding_indices_in_order + self.engine.get_tensor_shape(output_name) + for output_name in self.output_names ] + # TODO: Verify what this is for ? + # self.hidden_output_dtypes = [ + # unified_dtype_converter( + # self.engine.get_binding_dtype(idx), Frameworks.TORCH + # ) + # for idx in self.hidden_output_binding_indices_in_order + # ] + def _check_initialized(self) -> None: if not self.initialized: raise RuntimeError("PythonTorchTensorRTModule is not initialized.") @@ -217,12 +213,12 @@ def forward(self, *inputs: torch.Tensor) -> torch.Tensor | Tuple[torch.Tensor, . ), f"Wrong number of inputs, expect {len(self.input_names)} get {len(inputs)}." contiguous_inputs: List[torch.Tensor] = [i.contiguous() for i in inputs] - bindings: List[Any] = [None] * ( - len(self.input_names) - + len(self.output_names) - + len(self.hidden_output_names) - ) - + bindings = [] + # [None] * ( + # len(self.input_names) + # + len(self.output_names) + # # + len(self.hidden_output_names) # TODO: Verify if this is required + # ) for i, input_name in enumerate(self.input_names): if not contiguous_inputs[i].is_cuda: logger.warning( @@ -241,11 +237,9 @@ def forward(self, *inputs: torch.Tensor) -> torch.Tensor | Tuple[torch.Tensor, . contiguous_inputs[i].dtype == self.input_dtypes[i] ), f"Dtype mismatch for {i}th input({input_name}). Expect {self.input_dtypes[i]}, got {contiguous_inputs[i].dtype}." - idx = self.input_binding_indices_in_order[i] - bindings[idx] = contiguous_inputs[i].data_ptr() - - self.context.set_binding_shape( - idx, tuple(contiguous_inputs[i].shape) + bindings.append(contiguous_inputs[i].data_ptr()) + self.context.set_input_shape( + input_name, tuple(contiguous_inputs[i].shape) ) with ( @@ -258,26 +252,32 @@ def forward(self, *inputs: torch.Tensor) -> torch.Tensor | Tuple[torch.Tensor, . # create output tensors outputs: List[torch.Tensor] = [] - for i, idx in enumerate(self.output_binding_indices_in_order): - shape = tuple(self.context.get_binding_shape(idx)) + for i, output_name in enumerate(self.output_names): + shape = tuple(self.context.get_tensor_shape(output_name)) output = torch.empty( size=shape, dtype=self.output_dtypes[i], device=torch.cuda.current_device(), ) + bindings.append(output.data_ptr()) outputs.append(output) - bindings[idx] = output.data_ptr() - for i, idx in enumerate(self.hidden_output_binding_indices_in_order): - shape = tuple(self.context.get_binding_shape(idx)) + # TODO: Check what is this for ? + # for i, idx in enumerate(self.hidden_output_binding_indices_in_order): + # shape = tuple(self.context.get_binding_shape(idx)) - output = torch.empty( - size=shape, - dtype=self.hidden_output_dtypes[i], - device=torch.cuda.current_device(), - ) - bindings[idx] = output.data_ptr() + # output = torch.empty( + # size=shape, + # dtype=self.hidden_output_dtypes[i], + # device=torch.cuda.current_device(), + # ) + + # Assign tensor address appropriately + for idx in range(self.engine.num_io_tensors): + self.context.set_tensor_address( + self.engine.get_tensor_name(idx), bindings[idx] + ) with ( torch.autograd.profiler.record_function( @@ -286,9 +286,7 @@ def forward(self, *inputs: torch.Tensor) -> torch.Tensor | Tuple[torch.Tensor, . if self.profiling_enabled else nullcontext() ): - self.context.execute_async_v2( - bindings, torch.cuda.current_stream().cuda_stream - ) + self.context.execute_async_v3(torch.cuda.current_stream().cuda_stream) if len(outputs) == 1: return outputs[0] diff --git a/py/torch_tensorrt/fx/converters/acc_ops_converters.py b/py/torch_tensorrt/fx/converters/acc_ops_converters.py index 1765077930..f998ddb27a 100644 --- a/py/torch_tensorrt/fx/converters/acc_ops_converters.py +++ b/py/torch_tensorrt/fx/converters/acc_ops_converters.py @@ -3,30 +3,27 @@ import math import operator import warnings -from typing import cast, Dict, Optional, Sequence, Tuple, Union +from typing import Dict, Optional, Sequence, Tuple, Union, cast import numpy as np # @manual=//deeplearning/trt/python:py_tensorrt import tensorrt as trt import torch - -from ..converter_registry import tensorrt_converter - -from ..tracer.acc_tracer import acc_ops -from ..types import * # noqa: F403 from torch.fx.immutable_collections import immutable_list from torch.fx.node import Argument, Target - -from ..utils import get_dynamic_dims, unified_dtype_converter, Frameworks - -from .converter_utils import * # noqa: F403 +from torch_tensorrt.fx.converters.impl import activation, convolution from torch_tensorrt.fx.passes.lower_basic_pass import ( trt_transposed_linear, trt_transposed_matmul, ) from torch_tensorrt.fx.tracer.acc_tracer.acc_ops import contiguous -from torch_tensorrt.fx.converters.impl import activation, convolution + +from ..converter_registry import tensorrt_converter +from ..tracer.acc_tracer import acc_ops +from ..types import * # noqa: F403 +from ..utils import Frameworks, get_dynamic_dims, unified_dtype_converter +from .converter_utils import * # noqa: F403 _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -323,7 +320,7 @@ def acc_ops_pad_with_slice_layer( ) layer.set_input(4, value_const) - layer.mode = trt.SliceMode.FILL + layer.mode = trt.SampleMode.FILL set_layer_name(layer, target, name) return layer.get_output(0) @@ -840,7 +837,7 @@ def acc_ops_tile( shapes = [1] * len(dims) strides = [1] * len(dims) layer = network.add_slice(input_val, starts, shapes, strides) - layer.mode = trt.SliceMode.WRAP + layer.mode = trt.SampleMode.WRAP set_layer_name(layer, target, name) if has_dynamic_shape(input_val.shape): # type: ignore[union-attr] @@ -3536,9 +3533,9 @@ def acc_ops_interpolate( layer.scales = [1, 1] + list(scale_factor) if mode.lower() in ["linear", "bilinear", "trilinear"]: - layer.resize_mode = trt.ResizeMode.LINEAR + layer.resize_mode = trt.InterpolationMode.LINEAR else: - layer.resize_mode = trt.ResizeMode.NEAREST + layer.resize_mode = trt.InterpolationMode.NEAREST if (align_corners is not None) and align_corners: layer.coordinate_transformation = ( diff --git a/py/torch_tensorrt/fx/utils.py b/py/torch_tensorrt/fx/utils.py index 4202e1e96b..9acc53ffc8 100644 --- a/py/torch_tensorrt/fx/utils.py +++ b/py/torch_tensorrt/fx/utils.py @@ -1,18 +1,21 @@ from enum import Enum -from typing import Dict, List, Optional, Callable, Union +from typing import Callable, Dict, List, Optional, Union + import numpy as np -from packaging import version # @manual=//deeplearning/trt/python:py_tensorrt import tensorrt as trt import torch from functorch import make_fx from functorch.experimental import functionalize +from torch_tensorrt._utils import sanitized_torch_version from torch_tensorrt.fx.passes.lower_basic_pass import ( replace_op_with_indices, run_const_fold, ) -from torch_tensorrt._utils import sanitized_torch_version + +from packaging import version + from .types import Shape, TRTDataType @@ -45,6 +48,11 @@ class Frameworks(Enum): Frameworks.TORCH: torch.float32, Frameworks.TRT: trt.float32, }, + trt.bool: { + Frameworks.NUMPY: bool, + Frameworks.TORCH: torch.bool, + Frameworks.TRT: trt.bool, + }, } if trt.__version__ >= "7.0": @@ -89,10 +97,10 @@ def unified_dtype_converter( The equivalent data type in the requested framework. """ assert to in Frameworks, f"Expected valid Framework for translation, got {to}" - + trt_major_version = int(trt.__version__.split(".")[0]) if dtype in (np.int8, torch.int8, trt.int8): return DataTypeEquivalence[trt.int8][to] - elif trt.__version__ >= "7.0" and dtype in (np.bool_, torch.bool, trt.bool): + elif trt_major_version >= 7 and dtype in (np.bool_, torch.bool, trt.bool): return DataTypeEquivalence[trt.bool][to] elif dtype in (np.int32, torch.int32, trt.int32): return DataTypeEquivalence[trt.int32][to] diff --git a/tests/py/dynamo/runtime/test_convert_method_to_trt_engine.py b/tests/py/dynamo/runtime/test_convert_method_to_trt_engine.py index b10cae23fa..46a9ab392c 100644 --- a/tests/py/dynamo/runtime/test_convert_method_to_trt_engine.py +++ b/tests/py/dynamo/runtime/test_convert_method_to_trt_engine.py @@ -25,12 +25,10 @@ def forward(self, a, b): symbolic_traced_gm, "forward", inputs=[input_data_0, input_data_1] ) - # Deserialize the TensorRT engine - with trt.Logger() as logger, trt.Runtime(logger) as runtime: - engine = runtime.deserialize_cuda_engine(trt_engine_str) - # Inference on TRT Engine - py_trt_module = PythonTorchTensorRTModule(engine, ["a", "b"], ["output0"]) + py_trt_module = PythonTorchTensorRTModule( + trt_engine_str, ["a", "b"], ["output0"] + ) trt_output = py_trt_module(input_data_0, input_data_1).cpu() # Inference on PyTorch model From 39ca77df3345179ec1560bfb54da5975fc67ca6b Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Thu, 21 Mar 2024 16:53:35 -0700 Subject: [PATCH 012/122] chore: more fixes --- .../conversion/impl/elementwise/base.py | 1 + .../dynamo/conversion/impl/elementwise/ops.py | 1 + .../dynamo/conversion/impl/select.py | 23 ++++++++++++------- .../dynamo/conversion/impl/shape.py | 14 +++++++---- .../fx/converters/converter_utils.py | 10 +++++--- py/torch_tensorrt/fx/utils.py | 7 ++++++ tests/py/dynamo/conversion/test_erf_aten.py | 6 +---- tests/py/dynamo/conversion/test_neg_aten.py | 6 +---- 8 files changed, 42 insertions(+), 26 deletions(-) diff --git a/py/torch_tensorrt/dynamo/conversion/impl/elementwise/base.py b/py/torch_tensorrt/dynamo/conversion/impl/elementwise/base.py index 6664a67cfe..615cd0bd62 100644 --- a/py/torch_tensorrt/dynamo/conversion/impl/elementwise/base.py +++ b/py/torch_tensorrt/dynamo/conversion/impl/elementwise/base.py @@ -150,6 +150,7 @@ def convert_binary_elementwise( lhs_val, rhs_val = broadcast( ctx.net, lhs_val, rhs_val, f"{name}_lhs", f"{name}_rhs" ) + layer = ctx.net.add_elementwise(lhs_val, rhs_val, op_type) set_layer_name(layer, target, name, source_ir) output = layer.get_output(0) diff --git a/py/torch_tensorrt/dynamo/conversion/impl/elementwise/ops.py b/py/torch_tensorrt/dynamo/conversion/impl/elementwise/ops.py index d0f6d29482..77ec68629b 100644 --- a/py/torch_tensorrt/dynamo/conversion/impl/elementwise/ops.py +++ b/py/torch_tensorrt/dynamo/conversion/impl/elementwise/ops.py @@ -244,6 +244,7 @@ def add( lhs_val: Union[TRTTensor, int, float], rhs_val: Union[TRTTensor, int, float], ) -> TRTTensor: + return convert_binary_elementwise( ctx, target, source_ir, name, trt.ElementWiseOperation.SUM, lhs_val, rhs_val ) diff --git a/py/torch_tensorrt/dynamo/conversion/impl/select.py b/py/torch_tensorrt/dynamo/conversion/impl/select.py index db586be65f..955ef2bb76 100644 --- a/py/torch_tensorrt/dynamo/conversion/impl/select.py +++ b/py/torch_tensorrt/dynamo/conversion/impl/select.py @@ -1,5 +1,5 @@ import logging -from typing import Optional, Sequence, Union, cast +from typing import Any, Optional, cast import numpy as np import tensorrt as trt @@ -9,6 +9,7 @@ from torch_tensorrt.dynamo.conversion._ConversionContext import ConversionContext from torch_tensorrt.dynamo.conversion.converter_utils import ( broadcastable, + cast_trt_tensor, get_positive_dim, get_trt_tensor, to_numpy, @@ -81,7 +82,7 @@ def index( source_ir: Optional[SourceIR], name: str, input: TRTTensor, - index: Sequence[Union[TRTTensor, np.ndarray, torch.Tensor]], + index: Any, ) -> TRTTensor: adv_indx_indices = [] tensor_indices = [] @@ -90,7 +91,7 @@ def index( # is_numpy is a flag to specify if all the indices are numpy or torchTensor. # If any is not this flag will be set to False _LOGGER.debug( - f"Determining whether aten.index constant-index optimization can be invoked" + "Determining whether aten.index constant-index optimization can be invoked" ) is_numpy = all( isinstance(ind, (torch.Tensor, np.ndarray)) for ind in index if ind is not None @@ -123,7 +124,7 @@ def index( return identity_layer.get_output(0) elif len(tensor_indices) == 1: indices_tensor = get_trt_tensor( - ctx, tensor_indices[0], name + f"_parameter_to_fp32_tensor" + ctx, tensor_indices[0], name + "_parameter_to_fp32_tensor" ) index = adv_indx_indices[0] _LOGGER.debug(f"The advanced index indices is {adv_indx_indices}") @@ -204,7 +205,7 @@ def index( cum_adv_index = cum_adv_index + adv_index multiplier = multiplier * input_shape[adv_indx_indices[i]] cum_adv_index = get_trt_tensor( - ctx, cum_adv_index, name + f"_index_sum_intermediate" + ctx, cum_adv_index, name + "_index_sum_intermediate" ) else: multiplier = get_trt_tensor( @@ -255,6 +256,12 @@ def index( cum_adv_index_shape_layer, target, name + "_cum_adv_index_shape", source_ir ) cum_adv_index_shape_tensor = cum_adv_index_shape_layer.get_output(0) + cum_adv_index_shape_tensor = cast_trt_tensor( + ctx, + cum_adv_index_shape_tensor, + trt.int32, + name + "_cum_adv_index_shape_casted", + ) cum_adv_index_shape = cum_adv_index.shape _LOGGER.debug(f"The shape for cumulative adv index is {cum_adv_index_shape}") # check if all advanced indices are consecutive @@ -263,7 +270,7 @@ def index( adv_indx_count == adv_indx_indices[adv_indx_count - 1] - adv_indx_indices[0] + 1 ): - _LOGGER.debug(f"The indices are continuous in this case") + _LOGGER.debug("The indices are continuous in this case") concat_tensor_reshape.append( get_trt_tensor(ctx, -1, name + "_dynamic_concat") ) @@ -287,7 +294,7 @@ def index( source_ir, ) unfold_tensor = regular_index_shuffle_layer.get_output(0) - _LOGGER.debug(f"The tensor is unfolded now") + _LOGGER.debug("The tensor is unfolded now") _LOGGER.debug(f"The unfolded tensor shape is {unfold_tensor.shape}") # Transpose folded advanced indexed axis to its original location. @@ -342,7 +349,7 @@ def index( reshape_output = unfold_advanced_shuffle_layer.get_output(0) else: - _LOGGER.debug(f"The indices are not continuous in this case") + _LOGGER.debug("The indices are not continuous in this case") concat_final_tensor = [] concat_final_tensor.append(cum_adv_index_shape_tensor) for i in range(0, rank): diff --git a/py/torch_tensorrt/dynamo/conversion/impl/shape.py b/py/torch_tensorrt/dynamo/conversion/impl/shape.py index ef30b186c1..7551d58059 100644 --- a/py/torch_tensorrt/dynamo/conversion/impl/shape.py +++ b/py/torch_tensorrt/dynamo/conversion/impl/shape.py @@ -8,11 +8,14 @@ from torch.fx.node import Target from torch_tensorrt.dynamo._SourceIR import SourceIR from torch_tensorrt.dynamo.conversion._ConversionContext import ConversionContext -from torch_tensorrt.dynamo.conversion.converter_utils import to_numpy from torch_tensorrt.dynamo.conversion.impl.elementwise.base import ( convert_binary_elementwise, ) -from torch_tensorrt.fx.converters.converter_utils import set_layer_name +from torch_tensorrt.fx.converters.converter_utils import ( + Frameworks, + set_layer_name, + unified_dtype_converter, +) from torch_tensorrt.fx.types import TRTTensor @@ -48,16 +51,17 @@ def get_shape_with_dynamic_shape( """ # Ger real shape info for input_val input_shape = ctx.net.add_shape(input_val).get_output(0) - + # input_shape.dtype is int64 in TRT 10.0 + input_np_dtype = unified_dtype_converter(input_shape.dtype, Frameworks.NUMPY) scale_layer = ctx.net.add_constant( - input_shape.shape, np.ascontiguousarray(shape, dtype=np.int32) + input_shape.shape, np.ascontiguousarray(shape, dtype=input_np_dtype) ) set_layer_name(scale_layer, target, f"{name}_scale") scale_res = scale_layer.get_output(0) length = input_shape.shape[0] zero_layer = ctx.net.add_constant( - input_shape.shape, to_numpy(torch.zeros((length), dtype=torch.int32)) + input_shape.shape, np.zeros(length, dtype=input_np_dtype) ) set_layer_name(zero_layer, target, f"{name}_zeros") diff --git a/py/torch_tensorrt/fx/converters/converter_utils.py b/py/torch_tensorrt/fx/converters/converter_utils.py index 49bf401f58..510d4ef69b 100644 --- a/py/torch_tensorrt/fx/converters/converter_utils.py +++ b/py/torch_tensorrt/fx/converters/converter_utils.py @@ -1,8 +1,8 @@ import operator import warnings +from enum import Enum, auto from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union -from enum import Enum, auto import numpy as np # @manual=//deeplearning/trt/python:py_tensorrt @@ -20,7 +20,7 @@ TRTPluginFieldCollection, TRTTensor, ) -from ..utils import unified_dtype_converter, Frameworks +from ..utils import Frameworks, unified_dtype_converter class SourceIR(Enum): @@ -351,13 +351,17 @@ def prepend_ones( # compute the final shape. if has_dynamic_shape(tensor.shape): tensor_shape_layer = network.add_shape(tensor) + tensor_shape = tensor_shape_layer.get_output(0) + tensor_shape = type_cast( + network, "shape", name + "shape_casted", tensor_shape, trt.int32 + ) tensor_shape_layer.name = f"{name}_broadcast_orig_shape" prepend_shape_layer = network.add_constant( (num_prepend_ones,), np.ones((num_prepend_ones,), dtype=np.int32) ) prepend_shape_layer.name = f"{name}_broadcast_prepend_ones" reshape_dim_layer = network.add_concatenation( - [prepend_shape_layer.get_output(0), tensor_shape_layer.get_output(0)] + [prepend_shape_layer.get_output(0), tensor_shape] ) reshape_dim_layer.axis = 0 reshape_dim_layer.name = f"{name}_broadcast_final_shape" diff --git a/py/torch_tensorrt/fx/utils.py b/py/torch_tensorrt/fx/utils.py index 9acc53ffc8..5bef21b6be 100644 --- a/py/torch_tensorrt/fx/utils.py +++ b/py/torch_tensorrt/fx/utils.py @@ -38,6 +38,11 @@ class Frameworks(Enum): Frameworks.TORCH: torch.int32, Frameworks.TRT: trt.int32, }, + trt.int64: { + Frameworks.NUMPY: np.int64, + Frameworks.TORCH: torch.int64, + Frameworks.TRT: trt.int64, + }, trt.float16: { Frameworks.NUMPY: np.float16, Frameworks.TORCH: torch.float16, @@ -104,6 +109,8 @@ def unified_dtype_converter( return DataTypeEquivalence[trt.bool][to] elif dtype in (np.int32, torch.int32, trt.int32): return DataTypeEquivalence[trt.int32][to] + elif dtype in (np.int64, torch.int64, trt.int64): + return DataTypeEquivalence[trt.int64][to] elif dtype in (np.float16, torch.float16, trt.float16): return DataTypeEquivalence[trt.float16][to] elif dtype in (np.float32, torch.float32, trt.float32): diff --git a/tests/py/dynamo/conversion/test_erf_aten.py b/tests/py/dynamo/conversion/test_erf_aten.py index 3f52e436b4..d65788bc85 100644 --- a/tests/py/dynamo/conversion/test_erf_aten.py +++ b/tests/py/dynamo/conversion/test_erf_aten.py @@ -22,11 +22,7 @@ def forward(self, input): return torch.ops.aten.erf.default(input) inputs = [torch.randn(x, dtype=type)] - self.run_test( - erf(), - inputs, - precision=type, - ) + self.run_test(erf(), inputs, precision=type, output_dtypes=[type]) @parameterized.expand( [ diff --git a/tests/py/dynamo/conversion/test_neg_aten.py b/tests/py/dynamo/conversion/test_neg_aten.py index c49fc32c23..818d9c5a83 100644 --- a/tests/py/dynamo/conversion/test_neg_aten.py +++ b/tests/py/dynamo/conversion/test_neg_aten.py @@ -22,11 +22,7 @@ def forward(self, input): return torch.ops.aten.neg.default(input) inputs = [torch.randn(x, dtype=type)] - self.run_test( - neg(), - inputs, - precision=type, - ) + self.run_test(neg(), inputs, precision=type, output_dtypes=[type]) @parameterized.expand( [ From 5431ee37dfe879a9bd2cdfbe3779227228dc630b Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 25 Mar 2024 09:54:01 -0700 Subject: [PATCH 013/122] chore: update trt version --- py/requirements.txt | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/py/requirements.txt b/py/requirements.txt index 334f1e9c76..19c39c8647 100644 --- a/py/requirements.txt +++ b/py/requirements.txt @@ -5,5 +5,5 @@ pybind11==2.6.2 torch==2.3.0 torchvision==0.18.0 --extra-index-url https://pypi.ngc.nvidia.com -tensorrt==8.6.1 +tensorrt>=10.0 pyyaml diff --git a/pyproject.toml b/pyproject.toml index 5c42700ef8..6c4940013d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ requires = [ "cffi>=1.15.1", "typing-extensions>=4.7.0", "future>=0.18.3", - "tensorrt>=8.6,<8.7", + "tensorrt>=10.0", "torch >=2.3.0.dev,<2.4.0", "pybind11==2.6.2", "numpy", @@ -42,7 +42,7 @@ requires-python = ">=3.8" keywords = ["pytorch", "torch", "tensorrt", "trt", "ai", "artificial intelligence", "ml", "machine learning", "dl", "deep learning", "compiler", "dynamo", "torchscript", "inference"] dependencies = [ "torch >=2.3.0.dev,<2.4.0", - "tensorrt>=8.6,<8.7", + "tensorrt>=10.0", "packaging>=23", "numpy", "typing-extensions>=4.7.0", From 0c03de5ca11d7c77adf3ce8cbe609bd832eced4e Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 25 Mar 2024 18:51:39 -0700 Subject: [PATCH 014/122] chore: more updates --- core/conversion/converters/converter_util.cpp | 9 +++++++-- core/conversion/converters/converter_util.h | 3 +++ core/conversion/converters/impl/constant_pad.cpp | 9 +++------ core/conversion/converters/impl/conv_deconv.cpp | 2 +- core/conversion/converters/impl/cumsum.cpp | 2 +- core/conversion/converters/impl/expand.cpp | 9 ++++----- core/conversion/converters/impl/interpolate.cpp | 2 +- core/conversion/converters/impl/linear.cpp | 7 +++++-- core/conversion/converters/impl/select.cpp | 13 +++++-------- 9 files changed, 30 insertions(+), 26 deletions(-) diff --git a/core/conversion/converters/converter_util.cpp b/core/conversion/converters/converter_util.cpp index 3dcd2e9d80..39afe9945f 100644 --- a/core/conversion/converters/converter_util.cpp +++ b/core/conversion/converters/converter_util.cpp @@ -39,6 +39,12 @@ nvinfer1::ITensor* addPadding( } } +nvinfer1::ITensor* getShapeOutput(ConversionCtx* ctx, nvinfer1::ITensor* input_tensor, const std::string& name) { + nvinfer1::ITensor* input_shape = ctx->net->addShape(*input_tensor)->getOutput(0); + input_shape = castITensor(ctx, input_shape, nvinfer1::DataType::kINT32, name); + return input_shape; +} + nvinfer1::ITensor* addUnpadding( ConversionCtx* ctx, const torch::jit::Node* n, @@ -134,7 +140,7 @@ nvinfer1::ILayer* add_elementwise( } auto otherStaticShapeMask = tensor_to_const(ctx, thOtherStaticShapeMask); auto otherDynamicShapeMask = tensor_to_const(ctx, thOtherDynamicShapeMask); - auto selfShape = ctx->net->addShape(*self)->getOutput(0); + nvinfer1::ITensor* selfShape = getShapeOutput(ctx, self, std::string(name + "_shape_cast").c_str()); // size of dynamic dimension of other need to the same as that of // corresponding dimension of self auto otherDynamicShape = @@ -348,7 +354,6 @@ nvinfer1::ITensor* normalize_indices( auto neg_itensor = tensor_to_const(ctx, neg); // find the indices that = -1 auto signs = clamp(ctx, indices, neg_itensor, zero_itensor, "clamp layer for " + name); - // get the inputDim value where indices == -1, else 0 auto mul = add_elementwise(ctx, nvinfer1::ElementWiseOperation::kPROD, signs, input_dim, "prod layer for " + name); TORCHTRT_CHECK(mul, "Unable to create mul layer in normalize_indices"); diff --git a/core/conversion/converters/converter_util.h b/core/conversion/converters/converter_util.h index 3342302431..ad57c476e1 100644 --- a/core/conversion/converters/converter_util.h +++ b/core/conversion/converters/converter_util.h @@ -62,6 +62,9 @@ nvinfer1::ITensor* castITensor( nvinfer1::DataType dtype, const std::string& layer_name_prefix = ""); +// Get the shape of the input tensor and cast it to INT32 type +nvinfer1::ITensor* getShapeOutput(ConversionCtx* ctx, nvinfer1::ITensor* input_tensor, const std::string& name = ""); + // Freeze an at::Tensor in a IConstant layer nvinfer1::ITensor* tensor_to_const(ConversionCtx* ctx, at::Tensor t, const std::string& name = std::string()); diff --git a/core/conversion/converters/impl/constant_pad.cpp b/core/conversion/converters/impl/constant_pad.cpp index 20a091e730..42947e1c03 100644 --- a/core/conversion/converters/impl/constant_pad.cpp +++ b/core/conversion/converters/impl/constant_pad.cpp @@ -60,13 +60,10 @@ auto constant_pad_registrations TORCHTRT_UNUSED = RegisterNodeConversionPatterns if (ctx->input_is_dynamic) { // build the size using inetwork layers - auto shape_layer = ctx->net->addShape(*in); - TORCHTRT_CHECK(shape_layer, "Unable to create shape layer from node: " << *n); - shape_layer->setName((util::node_info(n) + "_shape").c_str()); auto total_padding_itensor = tensor_to_const(ctx, torch::tensor(total_padding, torch::kInt32)); - - auto add_layer = ctx->net->addElementWise( - *shape_layer->getOutput(0), *total_padding_itensor, nvinfer1::ElementWiseOperation::kSUM); + nvinfer1::ITensor* shapeOutput = getShapeOutput(ctx, in, (util::node_info(n) + "_shape").c_str()); + auto add_layer = + ctx->net->addElementWise(*shapeOutput, *total_padding_itensor, nvinfer1::ElementWiseOperation::kSUM); TORCHTRT_CHECK(add_layer, "Unable to create add layer from node: " << *n); add_layer->setName((util::node_info(n) + "_add").c_str()); slice_layer->setInput(2, *add_layer->getOutput(0)); diff --git a/core/conversion/converters/impl/conv_deconv.cpp b/core/conversion/converters/impl/conv_deconv.cpp index bf0df00c64..aa7acc9cc4 100644 --- a/core/conversion/converters/impl/conv_deconv.cpp +++ b/core/conversion/converters/impl/conv_deconv.cpp @@ -33,7 +33,7 @@ nvinfer1::ILayer* add_bias_layer( nvinfer1::Dims& input_dims, nvinfer1::Dims& output_padding, Weights& bias) { - nvinfer1::ITensor* input_shape = ctx->net->addShape(*input_tensor)->getOutput(0); + nvinfer1::ITensor* input_shape = getShapeOutput(ctx, input_tensor, std::string("bias_shape_cast").c_str()); // Add padding layer nvinfer1::ITensor* start; nvinfer1::ITensor* totalPadding; diff --git a/core/conversion/converters/impl/cumsum.cpp b/core/conversion/converters/impl/cumsum.cpp index 5c518fd635..f856ca5d4e 100644 --- a/core/conversion/converters/impl/cumsum.cpp +++ b/core/conversion/converters/impl/cumsum.cpp @@ -36,7 +36,7 @@ auto cumsum_registrations TORCHTRT_UNUSED = RegisterNodeConversionPatterns().pat torch::Tensor axis = torch::tensor(input_dims.d[dim], torch::kInt32); tripLimit = tensor_to_const(ctx, axis); } else { - nvinfer1::ITensor* inpShape = ctx->net->addShape(*in)->getOutput(0); + nvinfer1::ITensor* inpShape = getShapeOutput(ctx, in); torch::Tensor dimValue = torch::tensor(dim, torch::kInt32); nvinfer1::ITensor* axis = tensor_to_const(ctx, dimValue); tripLimit = ctx->net->addGather(*inpShape, *axis, 0)->getOutput(0); diff --git a/core/conversion/converters/impl/expand.cpp b/core/conversion/converters/impl/expand.cpp index 6b22fea8d4..0e68768e15 100644 --- a/core/conversion/converters/impl/expand.cpp +++ b/core/conversion/converters/impl/expand.cpp @@ -19,11 +19,11 @@ nvinfer1::ITensor* concat(int max_rank, int old_rank, ConversionCtx* ctx, nvinfe if (max_rank - old_rank > 0) { torch::Tensor thOne = torch::tensor(std::vector(max_rank - old_rank, 1), torch::kInt32); auto one_tensor = tensor_to_const(ctx, thOne); - auto in_shape_tensor = ctx->net->addShape(*tensor)->getOutput(0); + auto in_shape_tensor = getShapeOutput(ctx, tensor); nvinfer1::ITensor* const args[2] = {one_tensor, in_shape_tensor}; return ctx->net->addConcatenation(args, 2)->getOutput(0); } else { // max_rank - old_rank == 0 - return ctx->net->addShape(*tensor)->getOutput(0); + return getShapeOutput(ctx, tensor); } } @@ -221,8 +221,7 @@ auto expand_registrations TORCHTRT_UNUSED = auto targetDims = targetTensor->getDimensions(); LOG_DEBUG("(expand_as layer) Expand input from " << input_dims << " to " << targetDims); if (ctx->input_is_dynamic) { - return add_expand_dynamic( - ctx, n, in, ctx->net->addShape(*targetTensor)->getOutput(0), targetDims, false); + return add_expand_dynamic(ctx, n, in, getShapeOutput(ctx, targetTensor), targetDims, false); } else { return add_expand(ctx, n, in, targetDims); } @@ -357,7 +356,7 @@ auto expand_registrations TORCHTRT_UNUSED = if (ctx->input_is_dynamic) { auto start_tensor = tensor_to_const(ctx, torch::tensor(start_vec, torch::kInt32)); - auto expand_output_shape = ctx->net->addShape(*expand->getOutput(0))->getOutput(0); + auto expand_output_shape = getShapeOutput(ctx, expand->getOutput(0)); std::vector repeat_const_vec(repeat_shape_dims.nbDims, 1); repeat_const_vec[dim + 1] = repeats; auto repeat_const = tensor_to_const(ctx, torch::tensor(repeat_const_vec, torch::kInt32)); diff --git a/core/conversion/converters/impl/interpolate.cpp b/core/conversion/converters/impl/interpolate.cpp index 61721e3d2c..748e3c9e1a 100644 --- a/core/conversion/converters/impl/interpolate.cpp +++ b/core/conversion/converters/impl/interpolate.cpp @@ -91,7 +91,7 @@ void resize_layer_size( auto dynamic_shape_mask = tensor_to_const(ctx, th_dynamic_shape_mask); auto static_shape_mask = tensor_to_const(ctx, th_static_shape_mask); - auto input_shape = ctx->net->addShape(*in)->getOutput(0); + nvinfer1::ITensor* input_shape = getShapeOutput(ctx, in); auto dynamic_shape = ctx->net->addElementWise(*input_shape, *dynamic_shape_mask, nvinfer1::ElementWiseOperation::kPROD) ->getOutput(0); diff --git a/core/conversion/converters/impl/linear.cpp b/core/conversion/converters/impl/linear.cpp index 39f9cb055d..0e4452dec0 100644 --- a/core/conversion/converters/impl/linear.cpp +++ b/core/conversion/converters/impl/linear.cpp @@ -40,11 +40,14 @@ auto linear_registrations TORCHTRT_UNUSED = RegisterNodeConversionPatterns().pat in = in_shuffle->getOutput(0); } - // Convert w_tensor to ITensor + // Convert w_tensor to ITensor and broadcast 2d to 4d if needed auto weight = args[1].IValue()->toTensor(); auto weight_tensor = tensor_to_const(ctx, weight, util::node_info(n) + "_weight"); + auto weight_shape = util::toVec(weight_tensor->getDimensions()); + weight_tensor = addPadding(ctx, n, weight_tensor, in->getDimensions().nbDims, false, false); + auto mm_layer = ctx->net->addMatrixMultiply( - *in, nvinfer1::MatrixOperation::kNONE, *weight_tensor, nvinfer1::MatrixOperation::kNONE); + *in, nvinfer1::MatrixOperation::kNONE, *weight_tensor, nvinfer1::MatrixOperation::kTRANSPOSE); TORCHTRT_CHECK(mm_layer, "Unable to create linear layer from node: " << *n); mm_layer->setName(util::node_info(n).c_str()); diff --git a/core/conversion/converters/impl/select.cpp b/core/conversion/converters/impl/select.cpp index 8334205879..d6b49aa609 100644 --- a/core/conversion/converters/impl/select.cpp +++ b/core/conversion/converters/impl/select.cpp @@ -368,8 +368,7 @@ auto select_registrations TORCHTRT_UNUSED = int rank = inDims.nbDims; LOG_WARNING("If indices include negative values, the exported graph will produce incorrect results."); int adv_idx_count = adv_idx_indices.size(); - auto in_shape_itensor = ctx->net->addShape(*in)->getOutput(0); - + nvinfer1::ITensor* in_shape_itensor = getShapeOutput(ctx, in); std::vector dim_tensor_list; for (int i = 0; i < rank; i++) { auto dim_tensor = @@ -401,7 +400,7 @@ auto select_registrations TORCHTRT_UNUSED = // t: [x_1, x_2, ..., x_m, y_1, y_2, ..., y_n] -> t: [x_1*x_2* ...*x_m, y_1*y_2* ...*y_n] nvinfer1::ITensor* flatten_tensor = NULL; { - auto shuffle_shape_tensor = ctx->net->addShape(*shuffle_out)->getOutput(0); + nvinfer1::ITensor* shuffle_shape_tensor = getShapeOutput(ctx, shuffle_out); auto d0 = tensor_to_const(ctx, torch::tensor({1}, torch::kInt32)); for (int i = 0; i < adv_idx_count; i++) { auto dim_tensor = @@ -479,7 +478,7 @@ auto select_registrations TORCHTRT_UNUSED = nvinfer1::ITensor* reshape_output = NULL; { - auto cum_adv_index_shape_tensor = ctx->net->addShape(*cum_adv_index)->getOutput(0); + nvinfer1::ITensor* cum_adv_index_shape_tensor = getShapeOutput(ctx, cum_adv_index); // check if all advanced indices are consecutive. if (adv_idx_count == (adv_idx_indices[adv_idx_count - 1] - adv_idx_indices[0] + 1)) { // unfold regular index axes @@ -559,8 +558,7 @@ auto select_registrations TORCHTRT_UNUSED = bool dynamic_shape = ctx->input_is_dynamic; auto input_dim = in->getDimensions(); // add Shape Tensor - auto ishape_layer = ctx->net->addShape(*in); - auto ishape_tensor = ishape_layer->getOutput(0); // input shape + nvinfer1::ITensor* ishape_tensor = getShapeOutput(ctx, in); std::string node_name = n->outputs()[0]->debugName().c_str(); int startIdx = 0; @@ -605,6 +603,7 @@ auto select_registrations TORCHTRT_UNUSED = stride_.d[i] = 1; } } + if (!dynamic_shape) { auto slice_layer = ctx->net->addSlice(*in, start_, size_, stride_); LOG_DEBUG("start_:" << start_); @@ -617,7 +616,6 @@ auto select_registrations TORCHTRT_UNUSED = LOG_DEBUG("Using dynamic version of slice"); // start tensor at::Tensor start_tensor = torch::zeros({nbdims}).to(torch::kI32); - ; start_tensor[axis] = startIdx; auto start_itensor = tensor_to_const(ctx, start_tensor); @@ -647,7 +645,6 @@ auto select_registrations TORCHTRT_UNUSED = // calculate size auto size_itensor = get_slice_size(ctx, out_start, out_end, stride_itensor, nbdims, node_name); - // update slice layer auto slice_layer = ctx->net->addSlice(*in, start_, size_, stride_); slice_layer->setInput(1, *out_start); // start From 982dbd25b3ffd2f4edbf5bd8c2fc321f2a35ab56 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 25 Mar 2024 11:57:39 -0700 Subject: [PATCH 015/122] parent f39e89e3964bc3d6ea3a6989b1e4099e1bb3e6dd author Dheeraj Peri 1711393059 -0700 committer Dheeraj Peri 1711393072 -0700 chore: minor updates chore: Fix save failures chore: minor fixes chore: remove duplicate bert test case chore: remove comments chore: add load api chore: minor updates chore: minor updates chore: minor updates chore: more updates --- core/runtime/TRTEngine.cpp | 2 +- docsrc/user_guide/saving_models.rst | 71 +++++---- py/torch_tensorrt/_compile.py | 111 +++++++++++++- py/torch_tensorrt/dynamo/_compiler.py | 9 +- py/torch_tensorrt/dynamo/_defaults.py | 1 - py/torch_tensorrt/dynamo/_exporter.py | 142 +++++++++++------- py/torch_tensorrt/dynamo/_settings.py | 3 - .../lowering/test_aten_lowering_passes.py | 4 +- tests/py/dynamo/models/test_export_serde.py | 104 ++++++++----- tests/py/dynamo/models/test_models_export.py | 63 +------- tests/py/dynamo/models/test_output_format.py | 62 -------- 11 files changed, 311 insertions(+), 261 deletions(-) delete mode 100644 tests/py/dynamo/models/test_output_format.py diff --git a/core/runtime/TRTEngine.cpp b/core/runtime/TRTEngine.cpp index 92e5d7a8ff..7a046f6d94 100644 --- a/core/runtime/TRTEngine.cpp +++ b/core/runtime/TRTEngine.cpp @@ -241,7 +241,7 @@ std::string TRTEngine::to_str() const { exec_ctx->getEngine().getTensorDataType(out_binding_names[o].c_str())) << std::endl; } - ss << " }" << std::endl; + ss << " ]" << std::endl; ss << " Device: " << device_info << std::endl; ss << " Hardware Compatibility: " << (hardware_compatible ? "Enabled" : "Disabled") << std::endl; // clang-format on diff --git a/docsrc/user_guide/saving_models.rst b/docsrc/user_guide/saving_models.rst index 8379b44f0f..73fee6e23c 100644 --- a/docsrc/user_guide/saving_models.rst +++ b/docsrc/user_guide/saving_models.rst @@ -9,23 +9,22 @@ Saving models compiled with Torch-TensorRT :undoc-members: :show-inheritance: -Saving models compiled with Torch-TensorRT varies slightly with the `ir` that has been used for compilation. +Saving models compiled with Torch-TensorRT can be done using `torch_tensorrt.save` API. Dynamo IR ------------- -The output type of `ir=dynamo` compilation of Torch-TensorRT is `torch.export.ExportedProgram` object by default. -In addition, we provide a new parameter `output_format` in the `CompilationSetting` object provided before compilation. -The `output_format` can take the following options +The output type of `ir=dynamo` compilation of Torch-TensorRT is `torch.fx.GraphModule` object by default. +We can save this object in either `TorchScript` (`torch.jit.ScriptModule`) or `ExportedProgram` (`torch.export.ExportedProgram`) formats by +specifying the `output_format` flag. Here are the options `output_format` will accept -* `exported_program` (or) `ep` : This is the default. Returns an ExportedProgram -* `torchscript` (or) `ts` : This returns a TorchScript module -* `graph_module` (or) `fx` : This returns a torch.fx.GraphModule which can be traced into Torchscript to save to disk. +* `exported_program` : This is the default. We perform transformations on the graphmodule first and use `torch.export.save` to save the module. +* `torchscript` : We trace the graphmodule via `torch.jit.trace` and save it via `torch.jit.save`. -a) Torchscript +a) ExportedProgram ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -If you set the `output_format="torchscript"`, this will return a `ScriptModule` which can be serialized via torch.jit.save +Here's an example usage .. code-block:: python @@ -34,19 +33,17 @@ If you set the `output_format="torchscript"`, this will return a `ScriptModule` model = MyModel().eval().cuda() inputs = [torch.randn((1, 3, 224, 224)).cuda()] - # trt_ts is a torch.jit.ScriptModule object - trt_ts = torch_tensorrt.compile(model, ir="dynamo", inputs, output_format="torchscript") - torch.jit.save(trt_ts, "trt_model.ts") + # trt_ep is a torch.fx.GraphModule object + trt_gm = torch_tensorrt.compile(model, ir="dynamo", inputs) + torchtrt.save(trt_gm, "trt.ep", inputs=inputs) # Later, you can load it and run inference - model = torch.jit.load("trt_model.ts").cuda() + model = torch.export.load("trt.ep").module() model(*inputs) -b) ExportedProgram +b) Torchscript ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -`torch.export.ExportedProgram`, a new format introduced in Pytorch 2.X is the default return type of Torch-TensorRT compilation. - .. code-block:: python import torch @@ -54,30 +51,14 @@ b) ExportedProgram model = MyModel().eval().cuda() inputs = [torch.randn((1, 3, 224, 224)).cuda()] - # trt_ep is a torch.export.ExportedProgram object - trt_ep = torch_tensorrt.compile(model, ir="dynamo", inputs) - torch.export.save(trt_ep, "trt_model.ep") + # trt_gm is a torch.fx.GraphModule object + trt_gm = torch_tensorrt.compile(model, ir="dynamo", inputs) + torch_tensorrt.save(trt_gm, "trt.ts", output_format="torchscript", inputs=inputs) # Later, you can load it and run inference - model = torch.export.load("trt_model.ep") + model = torch.jit.load("trt.ts").cuda() model(*inputs) -c) GraphModule -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -We can also return a `torch.fx.GraphModule` object as the output of Torch-TensorRT compilation by setting `output_format="graph_module"`. -Internally, partitioning, lowering, conversion phases operate using GraphModule objects. These can be either traced into a Torchscript modules or -exported into `ExportedProgram` objects - -.. code-block:: python - - import torch - import torch_tensorrt - - model = MyModel().eval().cuda() - inputs = [torch.randn((1, 3, 224, 224)).cuda()] - # trt_gm is a torch.fx.GraphModule object - trt_gm = torch_tensorrt.compile(model, ir="dynamo", inputs, output_format="graph_module") Torchscript IR ------------- @@ -99,3 +80,21 @@ For `ir=ts`, this behavior stays the same in 2.X versions as well. model = torch.jit.load("trt_model.ts").cuda() model(*inputs) + +Loading the models +-------------------- + +We can load torchscript or exported_program models using `torch.jit.load` and `torch.export.load` APIs from PyTorch directly. +Alternatively, we provide a light wrapper `torch_tensorrt.load(file_path)` which can load either of the above model types. + +Here's an example usage + +.. code-block:: python + + import torch + import torch_tensorrt + + # file_path can be trt.ep or trt.ts file obtained via saving the model (refer to the above section) + inputs = [torch.randn((1, 3, 224, 224)).cuda()] + model = torch_tensorrt.load().module() + model(*inputs) \ No newline at end of file diff --git a/py/torch_tensorrt/_compile.py b/py/torch_tensorrt/_compile.py index 9dd816e633..7acf83124a 100644 --- a/py/torch_tensorrt/_compile.py +++ b/py/torch_tensorrt/_compile.py @@ -6,6 +6,7 @@ import torch import torch.fx +import torch_tensorrt.dynamo import torch_tensorrt.ts from torch_tensorrt._enums import dtype from torch_tensorrt._Input import Input @@ -26,10 +27,7 @@ logger = logging.getLogger(__name__) -__all__ = [ - "compile", - "convert_method_to_trt_engine", -] +__all__ = ["compile", "convert_method_to_trt_engine", "save", "load"] def _non_fx_input_interface( @@ -332,3 +330,108 @@ def convert_method_to_trt_engine( ) else: raise RuntimeError("Module is an unknown format or the ir requested is unknown") + + +def load(file_path: str = "") -> Any: + """ + Load either a Torchscript model or ExportedProgram. Autodetect the type using + try, except + """ + try: + logger.debug("Loading the provided file using torch.jit.load()") + ts_module = torch.jit.load(file_path) + return ts_module + except Exception: + logger.debug( + "Loading the provided file via torch.jit.load() failed with the following error", + exc_info=True, + ) + pass + + try: + logger.debug("Loading the provided file using torch.export.load()") + exp_program = torch.export.load(file_path) + return exp_program + except Exception: + logger.debug( + "Loading the provided file via torch.export.load() failed with the following error", + exc_info=True, + ) + raise ValueError( + "The file doesn't correspond to a valid Torchscript module or ExportedProgram. Please verify the file path." + ) + + +def save( + module: Any, + file_path: str = "", + *, + output_format: str = "exported_program", + inputs: Optional[Sequence[torch.Tensor]] = None, + retrace: bool = False, +) -> None: + """ + Save the model to disk in the specified output format. + Arguments: + module : Compiled Torch-TensorRT module (Options include torch.jit.ScriptModule | torch.export.ExportedProgram | torch.fx.GraphModule) + inputs (torch.Tensor): Torch input tensors + output_format: Format to save the model. Options include exported_program | torchscript. + retrace: When the module type is a fx.GraphModule, this option re-exports the graph using torch.export.export(strict=False) to save it. + This flag is experimental for now. + """ + module_type = _parse_module_type(module) + accepted_formats = {"exported_program", "torchscript"} + if inputs is not None and not all( + isinstance(input, torch.Tensor) for input in inputs + ): + raise ValueError( + "Not all inputs provided are torch.tensors. Please provide torch.tensors as inputs" + ) + if output_format not in accepted_formats: + raise ValueError( + f"Provided output_format {output_format} is not supported. Supported options are exported_program | torchscript" + ) + if not file_path: + raise ValueError("File path cannot be empty. Please provide a valid file path") + + if module_type == _ModuleType.nn: + raise ValueError( + "Input model is of type nn.Module. Saving nn.Module directly is not supported. Supported model types torch.jit.ScriptModule | torch.fx.GraphModule | torch.export.ExportedProgram." + ) + elif module_type == _ModuleType.ts: + if output_format == "exported_program": + raise ValueError( + "Provided model is a torch.jit.ScriptModule but the output_format specified is exported_program. Please verify the output_format" + ) + else: + torch.jit.save(module, file_path) + elif module_type == _ModuleType.ep: + if output_format == "torchscript": + raise ValueError( + "Provided model is a torch.export.ExportedProgram but the output_format specified is torchscript. Please verify the output_format" + ) + else: + torch.export.save(module, file_path) + elif module_type == _ModuleType.fx: + if inputs is None: + raise ValueError( + "Provided model is a torch.fx.GraphModule however the inputs are empty. Please provide valid torch.tensors as inputs to trace and save the model" + ) + # The module type is torch.fx.GraphModule + if output_format == "torchscript": + module_ts = torch.jit.trace(module, inputs) + torch.jit.save(module_ts, file_path) + else: + if not retrace: + from torch_tensorrt.dynamo._exporter import export + + exp_program = export(module, inputs) + torch.export.save(exp_program, file_path) + else: + from torch._higher_order_ops.torchbind import enable_torchbind_tracing + + with enable_torchbind_tracing(): + exp_program = torch.export.export( + module, tuple(inputs), strict=False + ) + torch.export.save(exp_program, file_path) diff --git a/py/torch_tensorrt/dynamo/_compiler.py b/py/torch_tensorrt/dynamo/_compiler.py index 6312532f1c..b321eabcb2 100644 --- a/py/torch_tensorrt/dynamo/_compiler.py +++ b/py/torch_tensorrt/dynamo/_compiler.py @@ -30,7 +30,6 @@ MIN_BLOCK_SIZE, NUM_AVG_TIMING_ITERS, OPTIMIZATION_LEVEL, - OUTPUT_FORMAT, PASS_THROUGH_BUILD_FAILURES, PRECISION, REFIT, @@ -48,7 +47,6 @@ dryrun_stats_display, parse_non_trt_nodes, ) -from torch_tensorrt.dynamo._exporter import export from torch_tensorrt.dynamo.conversion import ( CompilationSettings, UnsupportedOperatorException, @@ -102,9 +100,8 @@ def compile( enable_experimental_decompositions: bool = ENABLE_EXPERIMENTAL_DECOMPOSITIONS, dryrun: bool = DRYRUN, hardware_compatible: bool = HARDWARE_COMPATIBLE, - output_format: str = OUTPUT_FORMAT, **kwargs: Any, -) -> Union[ExportedProgram, torch.jit.ScriptModule, torch.fx.GraphModule]: +) -> torch.fx.GraphModule: """Compile a TorchScript module for NVIDIA GPUs using TensorRT Takes a existing TorchScript module and a set of settings to configure the compiler @@ -246,14 +243,12 @@ def compile( "dla_global_dram_size": dla_global_dram_size, "dryrun": dryrun, "hardware_compatible": hardware_compatible, - "output_format": output_format, } settings = CompilationSettings(**compilation_options) logger.info("Compilation Settings: %s\n", settings) trt_gm = compile_module(gm, inputs, settings) - trt_result = export(trt_gm, torch_inputs, output_format) - return trt_result + return trt_gm def compile_module( diff --git a/py/torch_tensorrt/dynamo/_defaults.py b/py/torch_tensorrt/dynamo/_defaults.py index ec038c0dba..3d48ab3def 100644 --- a/py/torch_tensorrt/dynamo/_defaults.py +++ b/py/torch_tensorrt/dynamo/_defaults.py @@ -26,7 +26,6 @@ REQUIRE_FULL_COMPILATION = False DRYRUN = False HARDWARE_COMPATIBLE = False -OUTPUT_FORMAT = "exported_program" def default_device() -> Device: diff --git a/py/torch_tensorrt/dynamo/_exporter.py b/py/torch_tensorrt/dynamo/_exporter.py index c7e2f37795..e9d166a1cc 100644 --- a/py/torch_tensorrt/dynamo/_exporter.py +++ b/py/torch_tensorrt/dynamo/_exporter.py @@ -1,3 +1,4 @@ +import copy import operator from typing import Any, Dict, Sequence, Tuple, cast @@ -6,8 +7,11 @@ from torch._subclasses.fake_tensor import FakeTensor from torch.export import ExportedProgram, ExportGraphSignature from torch.export.exported_program import ( + CustomObjArgument, InputKind, InputSpec, + ModuleCallEntry, + ModuleCallSignature, OutputKind, OutputSpec, TensorArgument, @@ -18,27 +22,16 @@ def export( gm: torch.fx.GraphModule, inputs: Sequence[torch.Tensor], - output_format: str, ) -> ExportedProgram: """Export the result of TensorRT compilation into the desired output format. Arguments: gm (torch.fx.GraphModule): Compiled Torch-TensorRT module, generated by ``torch_tensorrt.dynamo.compile`` inputs (torch.Tensor): Torch input tensors - output_format (str): Output format of the result of TRT compilation. Options include "exported_program" (or) "ep" | "torchscript" (or) "ts" | "graph_module" (or) "fx". Default is "exported_program" """ - if output_format == "torchscript" or output_format == "ts": - return torch.jit.trace(gm, inputs) - elif output_format == "exported_program" or output_format == "ep": - patched_module = transform(gm, inputs) - exp_program = create_trt_exp_program(patched_module) - return exp_program - elif output_format == "graph_module" or output_format == "fx": - return gm - else: - raise ValueError( - f"Invalid output format {output_format} specified. Supported options include exported_program (or) ep | torchscript (or) ts | graph_module (or) fx" - ) + patched_module = transform(gm, inputs) + exp_program = create_trt_exp_program(patched_module) + return exp_program def transform( @@ -55,6 +48,10 @@ def transform( Returns an inlined torch.fx.GraphModule """ + # Make a copy the graph since this function transforms the input graph and changes it's attributes. + # This transformed graph is meant to be consumed by `create_trt_exp_program` + gm = copy.deepcopy(gm) + # Run shape analysis _, outputs_map = partitioning.run_shape_analysis(gm, inputs) @@ -72,7 +69,9 @@ def transform( return gm -def lift(gm: torch.fx.GraphModule, graph_signature: Any) -> torch.fx.GraphModule: +def lift( + gm: torch.fx.GraphModule, graph_signature: Any +) -> Tuple[torch.fx.GraphModule, ExportGraphSignature, Dict[str, Any], Dict[str, Any]]: """ Given an unlifted fx.GraphModule, lift all parameters, buffers into placeholders. Arguments: @@ -86,6 +85,7 @@ def lift(gm: torch.fx.GraphModule, graph_signature: Any) -> torch.fx.GraphModule # exp_program.state_dict contains parameters and buffers whereas a graph_module's state_dict # has all parameters registered as torch.tensors. state_dict = gm.state_dict() + constants = {} fake_mode = detect_fake_mode( tuple(node.meta["val"] for node in gm.graph.nodes if node.op == "placeholder") @@ -100,52 +100,69 @@ def lift(gm: torch.fx.GraphModule, graph_signature: Any) -> torch.fx.GraphModule break # At first the user_inputs are only present in the graph_signature.input_specs and hence non_user_input_idx=0 - # The input_specs should be of the form [params, buffers, constant_tensors, user_inputs] + # The input_specs should be of the form [params, buffers, constant_tensors, custom_obj, user_inputs] non_user_input_idx = 0 for node in gm.graph.nodes: if node.op == "get_attr": - if node.target not in state_dict: - raise ValueError( - f"The get_attr node : {node.name} with target: {node.target} value could not be found in state_dict. Please check the input exported_program's graphmodule parameters." - ) - constant_tensor = state_dict[node.target] - input_kind = InputKind.CONSTANT_TENSOR + lift_val = None + input_kind = None - # state_dict has these parameters/buffers as torch.Tensors. We override them as torch.nn.Parameter/torch.Tensors respectively. - for name, _ in gm.named_parameters(): - if node.target == name: - input_kind = InputKind.PARAMETER - state_dict[name] = torch.nn.Parameter(state_dict[name]) - break - for name, _ in gm.named_buffers(): - if node.target == name: - input_kind = InputKind.BUFFER - break + if node.target not in state_dict: + constants[node.target] = getattr(gm, node.target) + input_kind = InputKind.CUSTOM_OBJ + lift_val = constants[node.target] + else: + lift_val = state_dict[node.target] + + input_kind = InputKind.CONSTANT_TENSOR + + # state_dict has these parameters/buffers as torch.Tensors. We override them as torch.nn.Parameter/torch.Tensors respectively. + for name, _ in gm.named_parameters(): + if node.target == name: + input_kind = InputKind.PARAMETER + state_dict[name] = torch.nn.Parameter(state_dict[name]) + break + for name, _ in gm.named_buffers(): + if node.target == name: + input_kind = InputKind.BUFFER + break + + assert lift_val is not None and input_kind is not None # Replace get_attr nodes with placeholder nodes and copy metadata. with gm.graph.inserting_before(first_user_input): - const_placeholder_node = gm.graph.placeholder(node.target) + # Ensure name doesn't contain period as it is used for submodules + const_placeholder_node = gm.graph.placeholder( + node.target.replace(".", "_") + ) # Copy the node meta into this new placeholder node const_placeholder_node.meta = node.meta - const_placeholder_node.meta["val"] = cast( - FakeTensor, - torch.empty_strided( - tuple(constant_tensor.shape), - tuple([1] * len(constant_tensor.shape)), - ), - ) + + if isinstance(lift_val, torch.Tensor): + const_placeholder_node.meta["val"] = cast( + FakeTensor, + torch.empty_strided( + tuple(lift_val.shape), + tuple([1] * len(lift_val.shape)), + ), + ) node.replace_all_uses_with(const_placeholder_node) gm.graph.erase_node(node) # Add these parameters/buffers/constants to the existing graph signature # before user inputs. These specs are looked up in the state_dict during ExportedProgram creation. + input_spec_arg = TensorArgument(name=const_placeholder_node.name) + if input_kind == InputKind.CUSTOM_OBJ: + input_spec_arg = CustomObjArgument( + name=const_placeholder_node.name, class_fqn="" + ) graph_signature.input_specs.insert( non_user_input_idx, InputSpec( kind=input_kind, - arg=TensorArgument(name=const_placeholder_node.name), + arg=input_spec_arg, target=node.target, ), ) @@ -154,7 +171,7 @@ def lift(gm: torch.fx.GraphModule, graph_signature: Any) -> torch.fx.GraphModule gm.graph.eliminate_dead_code() gm.graph.lint() - return gm, graph_signature, state_dict + return gm, graph_signature, state_dict, constants def get_duplicate_nodes( @@ -292,18 +309,30 @@ def create_trt_exp_program( input_specs=input_specs, output_specs=output_specs ) + module_call_graph = [ + ModuleCallEntry( + "", + ModuleCallSignature( + inputs=[], + outputs=[], + in_spec=gm.graph._codegen.pytree_info.in_spec, + out_spec=gm.graph._codegen.pytree_info.out_spec, + ), + ) + ] + # Lift parameters/buffers/constants in the graph # torch.export serialization expects them to be lifted - gm, trt_graph_signature, state_dict = lift(gm, trt_graph_signature) + gm, trt_graph_signature, state_dict, constants = lift(gm, trt_graph_signature) trt_exp_program = ExportedProgram( - gm, - gm.graph, - trt_graph_signature, - state_dict, - {}, - [], - [], + root=gm, + graph=gm.graph, + graph_signature=trt_graph_signature, + state_dict=state_dict, + range_constraints={}, + module_call_graph=module_call_graph, + constants=constants, ) return trt_exp_program @@ -330,9 +359,13 @@ def inline_trt_modules( num_outputs = len(outputs_map[trt_module_node.name]) # Insert a call_function node to perform inference on TRT engine with gm.graph.inserting_before(trt_module_node): + engine_name = f"{name}_engine" + setattr(gm, engine_name, trt_module.engine) + engine_node = gm.graph.get_attr(engine_name) + trt_node = gm.graph.call_function( torch.ops.tensorrt.execute_engine.default, - (trt_module_node.args, trt_module.engine), + (trt_module_node.args, engine_node), ) trt_node.meta["val"] = [] assert num_outputs > 0 @@ -348,6 +381,13 @@ def inline_trt_modules( ) ) + # meta["val"] should be a lighter version of a tensor. For eg: it should be a FakeTensor (with output shape and dtype properties) + # Lighter version of a custom_obj is not defined clearly. meta["val"] does not have any type expectations but + # for custom object nodes, it should be CustomObjArgument + engine_node.meta["val"] = CustomObjArgument( + name=engine_node.name, class_fqn="" + ) + if num_outputs == 1: # Insert getitem nodes as outputs (for export serialization to work) with gm.graph.inserting_after(trt_node): diff --git a/py/torch_tensorrt/dynamo/_settings.py b/py/torch_tensorrt/dynamo/_settings.py index c00b049f45..2420a227d8 100644 --- a/py/torch_tensorrt/dynamo/_settings.py +++ b/py/torch_tensorrt/dynamo/_settings.py @@ -19,7 +19,6 @@ MIN_BLOCK_SIZE, NUM_AVG_TIMING_ITERS, OPTIMIZATION_LEVEL, - OUTPUT_FORMAT, PASS_THROUGH_BUILD_FAILURES, PRECISION, REFIT, @@ -71,7 +70,6 @@ class CompilationSettings: TRT Engines. Prints detailed logs of the graph structure and nature of partitioning. Optionally saves the ouptut to a file if a string path is specified hardware_compatible (bool): Build the TensorRT engines compatible with GPU architectures other than that of the GPU on which the engine was built (currently works for NVIDIA Ampere and newer) - output_format (str): Output format of the result of TRT compilation. Options include "exported_program" (or) "ep" | "torchscript" (or) "ts" | "graph_module" (or) "fx". Default is "exported_program" """ precision: torch.dtype = PRECISION @@ -99,4 +97,3 @@ class CompilationSettings: dla_global_dram_size: int = DLA_GLOBAL_DRAM_SIZE dryrun: Union[bool, str] = DRYRUN hardware_compatible: bool = HARDWARE_COMPATIBLE - output_format: str = OUTPUT_FORMAT diff --git a/tests/py/dynamo/lowering/test_aten_lowering_passes.py b/tests/py/dynamo/lowering/test_aten_lowering_passes.py index bc75a8aa3d..b7c895ec11 100644 --- a/tests/py/dynamo/lowering/test_aten_lowering_passes.py +++ b/tests/py/dynamo/lowering/test_aten_lowering_passes.py @@ -1,7 +1,6 @@ import torch -from torch.testing._internal.common_utils import TestCase, run_tests - import torch_tensorrt +from torch.testing._internal.common_utils import TestCase, run_tests from ..testing_utilities import DECIMALS_OF_AGREEMENT, lower_graph_testing @@ -444,6 +443,7 @@ def forward(self, input, weight, bias): max_diff = float( torch.max(torch.abs(optimized_model_results - torch_model_results)) ) + self.assertAlmostEqual( max_diff, 0, diff --git a/tests/py/dynamo/models/test_export_serde.py b/tests/py/dynamo/models/test_export_serde.py index efa593890e..b6519815a4 100644 --- a/tests/py/dynamo/models/test_export_serde.py +++ b/tests/py/dynamo/models/test_export_serde.py @@ -42,18 +42,17 @@ def forward(self, x): } exp_program = torchtrt.dynamo.trace(model, **compile_spec) - trt_exp_program = torchtrt.dynamo.compile(exp_program, **compile_spec) - torch.export.save(trt_exp_program, "/tmp/trt.ep") - deser_trt_exp_program = torch.export.load("/tmp/trt.ep") - + trt_module = torchtrt.dynamo.compile(exp_program, **compile_spec) + torchtrt.save(trt_module, "/tmp/trt.ep", inputs=[input]) + deser_trt_module = torchtrt.load("/tmp/trt.ep").module() # Check Pyt and TRT exported program outputs - cos_sim = cosine_similarity(model(input), trt_exp_program(input)[0]) + cos_sim = cosine_similarity(model(input), trt_module(input)[0]) assertions.assertTrue( cos_sim > COSINE_THRESHOLD, msg=f"test_base_model_full_compile TRT outputs don't match with the original model. Cosine sim score: {cos_sim} Threshold: {COSINE_THRESHOLD}", ) # Check Pyt and deserialized TRT exported program outputs - cos_sim = cosine_similarity(model(input), deser_trt_exp_program(input)[0]) + cos_sim = cosine_similarity(model(input), deser_trt_module(input)[0]) assertions.assertTrue( cos_sim > COSINE_THRESHOLD, msg=f"test_base_model_full_compile TRT outputs don't match with the original model. Cosine sim score: {cos_sim} Threshold: {COSINE_THRESHOLD}", @@ -93,12 +92,12 @@ def forward(self, x): } exp_program = torchtrt.dynamo.trace(model, **compile_spec) - trt_exp_program = torchtrt.dynamo.compile(exp_program, **compile_spec) - torch.export.save(trt_exp_program, "/tmp/trt.ep") - deser_trt_exp_program = torch.export.load("/tmp/trt.ep") + trt_module = torchtrt.dynamo.compile(exp_program, **compile_spec) + torchtrt.save(trt_module, "/tmp/trt.ep", inputs=[input]) + deser_trt_module = torchtrt.load("/tmp/trt.ep").module() # Check Pyt and TRT exported program outputs outputs_pyt = model(input) - outputs_trt = trt_exp_program(input) + outputs_trt = trt_module(input) for idx in range(len(outputs_pyt)): cos_sim = cosine_similarity(outputs_pyt[idx], outputs_trt[idx]) assertions.assertTrue( @@ -107,7 +106,7 @@ def forward(self, x): ) # Check Pyt and deserialized TRT exported program outputs - outputs_trt_deser = deser_trt_exp_program(input) + outputs_trt_deser = deser_trt_module(input) for idx in range(len(outputs_pyt)): cos_sim = cosine_similarity(outputs_pyt[idx], outputs_trt_deser[idx]) assertions.assertTrue( @@ -145,16 +144,15 @@ def forward(self, x): ) ], "ir": ir, - "debug": True, } exp_program = torchtrt.dynamo.trace(model, **compile_spec) - trt_exp_program = torchtrt.dynamo.compile(exp_program, **compile_spec) - torch.export.save(trt_exp_program, "/tmp/trt.ep") - deser_trt_exp_program = torch.export.load("/tmp/trt.ep") + trt_module = torchtrt.dynamo.compile(exp_program, **compile_spec) + torchtrt.save(trt_module, "/tmp/trt.ep", inputs=[input]) + deser_trt_module = torchtrt.load("/tmp/trt.ep").module() # Check Pyt and TRT exported program outputs outputs_pyt = model(input) - outputs_trt = trt_exp_program(input) + outputs_trt = trt_module(input) for idx in range(len(outputs_pyt)): cos_sim = cosine_similarity(outputs_pyt[idx], outputs_trt[idx]) assertions.assertTrue( @@ -163,7 +161,7 @@ def forward(self, x): ) # Check Pyt and deserialized TRT exported program outputs - outputs_trt_deser = deser_trt_exp_program(input) + outputs_trt_deser = deser_trt_module(input) for idx in range(len(outputs_pyt)): cos_sim = cosine_similarity(outputs_pyt[idx], outputs_trt_deser[idx]) assertions.assertTrue( @@ -207,12 +205,11 @@ def forward(self, x): } exp_program = torchtrt.dynamo.trace(model, **compile_spec) - trt_exp_program = torchtrt.dynamo.compile(exp_program, **compile_spec) - torch.export.save(trt_exp_program, "/tmp/trt.ep") - deser_trt_exp_program = torch.export.load("/tmp/trt.ep") - + trt_module = torchtrt.dynamo.compile(exp_program, **compile_spec) + torchtrt.save(trt_module, "/tmp/trt.ep", inputs=[input]) + deser_trt_module = torchtrt.load("/tmp/trt.ep").module() outputs_pyt = model(input) - outputs_trt = trt_exp_program(input) + outputs_trt = trt_module(input) for idx in range(len(outputs_pyt)): cos_sim = cosine_similarity(outputs_pyt[idx], outputs_trt[idx]) assertions.assertTrue( @@ -220,7 +217,7 @@ def forward(self, x): msg=f"test_hybrid_relu_fallback TRT outputs don't match with the original model. Cosine sim score: {cos_sim} Threshold: {COSINE_THRESHOLD}", ) - outputs_trt_deser = deser_trt_exp_program(input) + outputs_trt_deser = deser_trt_module(input) for idx in range(len(outputs_pyt)): cos_sim = cosine_similarity(outputs_pyt[idx], outputs_trt_deser[idx]) assertions.assertTrue( @@ -248,19 +245,18 @@ def test_resnet18(ir): } exp_program = torchtrt.dynamo.trace(model, **compile_spec) - trt_exp_program = torchtrt.dynamo.compile(exp_program, **compile_spec) - torch.export.save(trt_exp_program, "/tmp/trt.ep") - deser_trt_exp_program = torch.export.load("/tmp/trt.ep") - + trt_module = torchtrt.dynamo.compile(exp_program, **compile_spec) + torchtrt.save(trt_module, "/tmp/trt.ep", inputs=[input]) + deser_trt_module = torchtrt.load("/tmp/trt.ep").module() outputs_pyt = model(input) - outputs_trt = trt_exp_program(input) + outputs_trt = trt_module(input) cos_sim = cosine_similarity(outputs_pyt, outputs_trt[0]) assertions.assertTrue( cos_sim > COSINE_THRESHOLD, msg=f"test_resnet18 TRT outputs don't match with the original model. Cosine sim score: {cos_sim} Threshold: {COSINE_THRESHOLD}", ) - outputs_trt_deser = deser_trt_exp_program(input) + outputs_trt_deser = deser_trt_module(input) cos_sim = cosine_similarity(outputs_pyt, outputs_trt_deser[0]) assertions.assertTrue( @@ -303,12 +299,12 @@ def forward(self, x): } exp_program = torchtrt.dynamo.trace(model, **compile_spec) - trt_exp_program = torchtrt.dynamo.compile(exp_program, **compile_spec) - torch.export.save(trt_exp_program, "/tmp/trt.ep") - deser_trt_exp_program = torch.export.load("/tmp/trt.ep") + trt_module = torchtrt.dynamo.compile(exp_program, **compile_spec) + torchtrt.save(trt_module, "/tmp/trt.ep", inputs=[input]) + deser_trt_module = torchtrt.load("/tmp/trt.ep").module() outputs_pyt = model(input) - outputs_trt = trt_exp_program(input) + outputs_trt = trt_module(input) for idx in range(len(outputs_pyt)): cos_sim = cosine_similarity(outputs_pyt[idx], outputs_trt[idx]) @@ -317,10 +313,50 @@ def forward(self, x): msg=f"test_hybrid_conv_fallback TRT outputs don't match with the original model. Cosine sim score: {cos_sim} Threshold: {COSINE_THRESHOLD}", ) - outputs_trt_deser = deser_trt_exp_program(input) + outputs_trt_deser = deser_trt_module(input) for idx in range(len(outputs_pyt)): cos_sim = cosine_similarity(outputs_pyt[idx], outputs_trt_deser[idx]) assertions.assertTrue( cos_sim > COSINE_THRESHOLD, msg=f"test_hybrid_conv_fallback deserialized TRT outputs don't match with the original model. Cosine sim score: {cos_sim} Threshold: {COSINE_THRESHOLD}", ) + + +@pytest.mark.unit +def test_save_load_ts(ir): + """ + This tests save/load API on Torchscript format (model still compiled using dynamo workflow) + """ + + class MyModule(torch.nn.Module): + def __init__(self): + super().__init__() + self.conv = torch.nn.Conv2d(3, 16, 3, stride=1, bias=True) + self.relu = torch.nn.ReLU() + + def forward(self, x): + conv = self.conv(x) + relu = self.relu(conv) + mul = relu * 0.5 + return mul + + model = MyModule().eval().cuda() + input = torch.randn((1, 3, 224, 224)).to("cuda") + + trt_gm = torchtrt.compile(model, ir=ir, inputs=[input], min_block_size=1) + assertions.assertTrue( + isinstance(trt_gm, torch.fx.GraphModule), + msg=f"test_save_load_ts output type does not match with torch.fx.GraphModule", + ) + outputs_trt = trt_gm(input) + # Save it as torchscript representation + torchtrt.save(trt_gm, "/tmp/trt.ts", output_format="torchscript", inputs=[input]) + + trt_ts_module = torchtrt.load("/tmp/trt.ts") + outputs_trt_deser = trt_ts_module(input) + + cos_sim = cosine_similarity(outputs_trt, outputs_trt_deser) + assertions.assertTrue( + cos_sim > COSINE_THRESHOLD, + msg=f"test_save_load_ts TRT outputs don't match with the original model. Cosine sim score: {cos_sim} Threshold: {COSINE_THRESHOLD}", + ) diff --git a/tests/py/dynamo/models/test_models_export.py b/tests/py/dynamo/models/test_models_export.py index fd7b40592a..84f6bf7a36 100644 --- a/tests/py/dynamo/models/test_models_export.py +++ b/tests/py/dynamo/models/test_models_export.py @@ -110,11 +110,6 @@ def test_bert_base_uncased(ir): model = BertModel.from_pretrained("bert-base-uncased").cuda().eval() input = torch.randint(0, 1, (1, 14), dtype=torch.int32).to("cuda") input2 = torch.randint(0, 1, (1, 14), dtype=torch.int32).to("cuda") - model = ( - transformers_trace(model, input_names=["input_ids", "attention_mask"]) - .eval() - .cuda() - ) compile_spec = { "inputs": [ @@ -133,7 +128,7 @@ def test_bert_base_uncased(ir): "enabled_precisions": {torch.float}, "truncate_long_and_double": True, "ir": ir, - "min_block_size": 10, + "min_block_size": 15, } trt_mod = torchtrt.compile(model, **compile_spec) model_outputs = model(input, input2) @@ -142,58 +137,9 @@ def test_bert_base_uncased(ir): len(model_outputs) == len(trt_model_outputs), msg=f"Number of outputs for BERT model compilation is different with Pytorch {len(model_outputs)} and TensorRT {len(trt_model_outputs)}. Please check the compilation.", ) - for index, key in enumerate(model_outputs): - out, trt_out = model_outputs[key], trt_model_outputs[index] - cos_sim = cosine_similarity(out, trt_out) - assertions.assertTrue( - cos_sim > COSINE_THRESHOLD, - msg=f"HF BERT base-uncased TRT outputs don't match with the original model. Cosine sim score: {cos_sim} Threshold: {COSINE_THRESHOLD}", - ) - - # Clean up model env - torch._dynamo.reset() - - -@pytest.mark.unit -def test_bert_base_uncased(ir): - model = BertModel.from_pretrained("bert-base-uncased").cuda().eval() - input = torch.randint(0, 1, (1, 14), dtype=torch.int32).to("cuda") - input2 = torch.randint(0, 1, (1, 14), dtype=torch.int32).to("cuda") - model = ( - transformers_trace(model, input_names=["input_ids", "attention_mask"]) - .eval() - .cuda() - ) - compile_spec = { - "inputs": [ - torchtrt.Input( - input.shape, - dtype=input.dtype, - format=torch.contiguous_format, - ), - torchtrt.Input( - input.shape, - dtype=input.dtype, - format=torch.contiguous_format, - ), - ], - "device": torchtrt.Device("cuda:0"), - "enabled_precisions": {torch.float}, - "truncate_long_and_double": True, - "ir": ir, - "min_block_size": 10, - "torch_executed_ops": {"torch.ops.aten.gelu.default"}, - } - trt_mod = torchtrt.compile(model, **compile_spec) - model_outputs = model(input, input2) - trt_model_outputs = trt_mod(input, input2) - assertions.assertTrue( - len(model_outputs) == len(trt_model_outputs), - msg=f"Number of outputs for BERT model compilation is different with Pytorch {len(model_outputs)} and TensorRT {len(trt_model_outputs)}. Please check the compilation.", - ) - for index, key in enumerate(model_outputs): - out, trt_out = model_outputs[key], trt_model_outputs[index] + for key, _ in model_outputs.items(): + out, trt_out = model_outputs[key], trt_model_outputs[key] cos_sim = cosine_similarity(out, trt_out) assertions.assertTrue( cos_sim > COSINE_THRESHOLD, @@ -203,9 +149,6 @@ def test_bert_base_uncased(ir): # Clean up model env torch._dynamo.reset() - with torch.no_grad(): - torch.cuda.empty_cache() - @pytest.mark.unit def test_resnet18_half(ir): diff --git a/tests/py/dynamo/models/test_output_format.py b/tests/py/dynamo/models/test_output_format.py deleted file mode 100644 index 3d2e747ceb..0000000000 --- a/tests/py/dynamo/models/test_output_format.py +++ /dev/null @@ -1,62 +0,0 @@ -import unittest - -import pytest -import timm -import torch -import torch_tensorrt as torchtrt -import torchvision.models as models -from torch_tensorrt.dynamo.utils import COSINE_THRESHOLD, cosine_similarity - -assertions = unittest.TestCase() - - -@pytest.mark.unit -def test_output_format(ir): - """ - This tests output_format type in the compilation setting - """ - - class MyModule(torch.nn.Module): - def __init__(self): - super().__init__() - self.conv = torch.nn.Conv2d(3, 16, 3, stride=1, bias=True) - self.relu = torch.nn.ReLU() - - def forward(self, x): - conv = self.conv(x) - relu = self.relu(conv) - mul = relu * 0.5 - return mul - - model = MyModule().eval().cuda() - input = torch.randn((1, 3, 224, 224)).to("cuda") - - trt_ep = torchtrt.compile(model, ir="dynamo", inputs=[input], min_block_size=1) - assertions.assertTrue( - isinstance(trt_ep, torch.export.ExportedProgram), - msg=f"test_output_format output type does not match with torch.export.ExportedProgram", - ) - - trt_ts = torchtrt.compile( - model, - ir="dynamo", - inputs=[input], - min_block_size=1, - output_format="torchscript", - ) - assertions.assertTrue( - isinstance(trt_ts, torch.jit.ScriptModule), - msg=f"test_output_format output type does not match with torch.jit.ScriptModule", - ) - - trt_gm = torchtrt.compile( - model, - ir="dynamo", - inputs=[input], - min_block_size=1, - output_format="graph_module", - ) - assertions.assertTrue( - isinstance(trt_gm, torch.fx.GraphModule), - msg=f"test_output_format output type does not match with torch.fx.GraphModule", - ) From 1ae46e9c3cfd1fe3d13beeac28919fc6e24b1cf2 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Wed, 27 Mar 2024 09:07:47 -0700 Subject: [PATCH 016/122] chore: more updates --- core/conversion/converters/impl/conv_deconv.cpp | 9 ++++----- core/conversion/evaluators/eval_util.cpp | 14 +++++--------- py/requirements.txt | 2 +- pyproject.toml | 4 ++-- tests/core/partitioning/test_loading_model.cpp | 2 +- tests/cpp/test_compiled_modules.cpp | 2 +- tests/cpp/test_modules_as_engines.cpp | 2 +- 7 files changed, 15 insertions(+), 20 deletions(-) diff --git a/core/conversion/converters/impl/conv_deconv.cpp b/core/conversion/converters/impl/conv_deconv.cpp index aa7acc9cc4..a779a3c1b9 100644 --- a/core/conversion/converters/impl/conv_deconv.cpp +++ b/core/conversion/converters/impl/conv_deconv.cpp @@ -146,9 +146,9 @@ bool add_conv_deconv(ConversionCtx* ctx, const torch::jit::Node* n, args& args) // TensorRT expects nbSpatialDims = 2 or 3 filter_dim = util::unsqueezeDims(filter_dim, filter_dim.nbDims, 1, false); // Reshape input dimensions - in = addPadding(ctx, n, in, 4); + in = addPadding(ctx, n, in, 4, true, true, std::string(util::node_info(n) + "_input_shuffle")); LOG_DEBUG("Reshaping input dimensions to: " << in->getDimensions()); - kernel = addPadding(ctx, n, kernel, 4); + kernel = addPadding(ctx, n, kernel, 4, true, true, std::string(util::node_info(n) + "_kernel_shuffle")); LOG_DEBUG("Reshaping kernel dimensions to: " << kernel->getDimensions()); if (transposed) { num_output_maps = kernel_dims.d[1]; @@ -291,11 +291,10 @@ bool add_conv_deconv(ConversionCtx* ctx, const torch::jit::Node* n, args& args) // shape of convolution's weight: [out, in/groups, ...] auto conv = ctx->net->addConvolutionNd(*in, w.shape.d[0], w.kernel_shape, w.data, bias.data); TORCHTRT_CHECK(conv, "Unable to create convolution layer from node: " << *n); - conv->setStrideNd(stride); conv->setPaddingMode(nvinfer1::PaddingMode::kEXPLICIT_ROUND_DOWN); - conv->setPaddingNd(padding); - conv->setPostPadding(out_padding); + conv->setPrePadding(padding); + conv->setPostPadding(padding); conv->setDilationNd(dilation); conv->setNbGroups(groups); new_layer = conv; diff --git a/core/conversion/evaluators/eval_util.cpp b/core/conversion/evaluators/eval_util.cpp index 71b6de9eb2..9b6139073d 100644 --- a/core/conversion/evaluators/eval_util.cpp +++ b/core/conversion/evaluators/eval_util.cpp @@ -34,11 +34,8 @@ c10::IValue dynamic_size_layer(ConversionCtx* ctx, const torch::jit::Node* n, kw auto in = args.at(n->input(0)).ITensorOrFreeze(ctx); auto input_dims = in->getDimensions(); LOG_DEBUG("Input dimensions: " << input_dims); - - auto shape_layer = ctx->net->addShape(*in); - TORCHTRT_CHECK(shape_layer, "Unable to create shape layer from node: " << *n); - auto shape_1d_tensor = shape_layer->getOutput(0); - + nvinfer1::ITensor* shape_1d_tensor = torch_tensorrt::core::conversion::converters::getShapeOutput( + ctx, in, std::string(util::node_info(n) + "_dynamic_shape_layer_cast").c_str()); if (n->inputs().size() != 1) { auto maxDim = static_cast(in->getDimensions().nbDims); auto dim = args.at(n->input(1)).unwrapToInt(); @@ -423,13 +420,12 @@ c10::optional newTensorLikeImplementation( // broadcast constant to output shape std::vector start_vec(self->getDimensions().nbDims, 0); auto start_offset = util::toDims(c10::IntArrayRef(start_vec)); - auto shape_layer = ctx->net->addShape(*self); - TORCHTRT_CHECK(shape_layer, "Unable to create shape layer from node: " << *n); - shape_layer->setName((util::node_info(n) + "_shape").c_str()); + nvinfer1::ITensor* shape_output = torch_tensorrt::core::conversion::converters::getShapeOutput( + ctx, self, std::string(util::node_info(n) + "_shape").c_str()); // slice implements expand auto slice_layer = ctx->net->addSlice(*constant_itensor, start_offset, self->getDimensions(), start_offset); TORCHTRT_CHECK(slice_layer, "Unable to create slice layer from node: " << *n); - slice_layer->setInput(2, *shape_layer->getOutput(0)); + slice_layer->setInput(2, *shape_output); slice_layer->setName((util::node_info(n) + "_slice").c_str()); auto out_tensor = ctx->AssociateValueAndTensor(n->outputs()[0], slice_layer->getOutput(0)); LOG_DEBUG("Output tensor shape: " << out_tensor->getDimensions()); diff --git a/py/requirements.txt b/py/requirements.txt index 19c39c8647..565395b59b 100644 --- a/py/requirements.txt +++ b/py/requirements.txt @@ -5,5 +5,5 @@ pybind11==2.6.2 torch==2.3.0 torchvision==0.18.0 --extra-index-url https://pypi.ngc.nvidia.com -tensorrt>=10.0 +tensorrt==10.0.0b6 pyyaml diff --git a/pyproject.toml b/pyproject.toml index 6c4940013d..7468484f48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ requires = [ "cffi>=1.15.1", "typing-extensions>=4.7.0", "future>=0.18.3", - "tensorrt>=10.0", + "tensorrt==10.0.0b6", "torch >=2.3.0.dev,<2.4.0", "pybind11==2.6.2", "numpy", @@ -42,7 +42,7 @@ requires-python = ">=3.8" keywords = ["pytorch", "torch", "tensorrt", "trt", "ai", "artificial intelligence", "ml", "machine learning", "dl", "deep learning", "compiler", "dynamo", "torchscript", "inference"] dependencies = [ "torch >=2.3.0.dev,<2.4.0", - "tensorrt>=10.0", + "tensorrt==10.0.0b6", "packaging>=23", "numpy", "typing-extensions>=4.7.0", diff --git a/tests/core/partitioning/test_loading_model.cpp b/tests/core/partitioning/test_loading_model.cpp index b42368fe3e..67c42caef2 100644 --- a/tests/core/partitioning/test_loading_model.cpp +++ b/tests/core/partitioning/test_loading_model.cpp @@ -7,7 +7,7 @@ #ifndef DISABLE_TEST_IN_CI -TEST(Partitioning, ComputeResNet50FallbackGraphCorrectly) { +TEST(Partitioning, ComputeConditionalLoadingGraphCorrectly) { torch::jit::script::Module mod; try { mod = torch::jit::load("tests/modules/conditional_scripted.jit.pt"); diff --git a/tests/cpp/test_compiled_modules.cpp b/tests/cpp/test_compiled_modules.cpp index 62bae5756d..7def168249 100644 --- a/tests/cpp/test_compiled_modules.cpp +++ b/tests/cpp/test_compiled_modules.cpp @@ -58,7 +58,7 @@ INSTANTIATE_TEST_SUITE_P( PathAndInput({"tests/modules/resnet18_scripted.jit.pt", {{1, 3, 224, 224}}, {at::kFloat}}), PathAndInput({"tests/modules/mobilenet_v2_scripted.jit.pt", {{1, 3, 224, 224}}, {at::kFloat}}), PathAndInput({"tests/modules/efficientnet_b0_scripted.jit.pt", {{1, 3, 224, 224}}, {at::kFloat}}), - PathAndInput({"tests/modules/bert_base_uncased_traced.jit.pt", {{1, 14}, {1, 14}}, {at::kInt, at::kInt}}), + PathAndInput({"tests/modules/bert_base_uncased_traced.jit.pt", {{1, 14}, {1, 14}}, {at::kInt, at::kInt}}))); // NOTE: ViT tests are disabled until Python 3.11 issue is resolved // https://github.com/huggingface/pytorch-image-models/issues/1946 PathAndInput({"tests/modules/vit_scripted.jit.pt", // {{1, 3, 224, 224}}, {at::kFloat}}))); diff --git a/tests/cpp/test_modules_as_engines.cpp b/tests/cpp/test_modules_as_engines.cpp index 4cb9dd9f8d..cc9fdd24a4 100644 --- a/tests/cpp/test_modules_as_engines.cpp +++ b/tests/cpp/test_modules_as_engines.cpp @@ -29,7 +29,7 @@ INSTANTIATE_TEST_SUITE_P( testing::Values( PathAndInput({"tests/modules/resnet18_scripted.jit.pt", {{1, 3, 224, 224}}, {at::kFloat}}), PathAndInput({"tests/modules/mobilenet_v2_scripted.jit.pt", {{1, 3, 224, 224}}, {at::kFloat}}), - PathAndInput({"tests/modules/efficientnet_b0_scripted.jit.pt", {{1, 3, 224, 224}}, {at::kFloat}}), + PathAndInput({"tests/modules/efficientnet_b0_scripted.jit.pt", {{1, 3, 224, 224}}, {at::kFloat}}))); // NOTE: ViT tests are disabled until Python 3.11 issue is resolved // https://github.com/huggingface/pytorch-image-models/issues/1946 PathAndInput({"tests/modules/vit_scripted.jit.pt", // {{1, 3, 224, 224}}, {at::kFloat}}))); From beb59206d082d8f0842ccca6d30b714dffccc07f Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Wed, 27 Mar 2024 09:22:53 -0700 Subject: [PATCH 017/122] chore: Update versions --- README.md | 4 ++-- WORKSPACE | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 875b640304..eecae762cf 100644 --- a/README.md +++ b/README.md @@ -116,10 +116,10 @@ torch.jit.save(trt_ts_module, "trt_torchscript_module.ts") # save the TRT embedd These are the following dependencies used to verify the testcases. Torch-TensorRT can work with other versions, but the tests are not guaranteed to pass. - Bazel 5.2.0 -- Libtorch 2.3.0.dev (latest nightly) (built with CUDA 12.1) +- Libtorch 2.3.0 (built with CUDA 12.1) - CUDA 12.1 - cuDNN 8.9.5 -- TensorRT 8.6.1 +- TensorRT 10.0.0.6 ## Prebuilt Binaries and Wheel files diff --git a/WORKSPACE b/WORKSPACE index edc5c9a050..76e9a602fa 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -81,10 +81,10 @@ http_archive( http_archive( name = "tensorrt", build_file = "@//third_party/tensorrt/archive:BUILD", - sha256 = "0f8157a5fc5329943b338b893591373350afa90ca81239cdadd7580cd1eba254", - strip_prefix = "TensorRT-8.6.1.6", + sha256 = "7ad5c8eab8fe8803bef5d8cee54ec1f407797ac8edb8ea3186cb9f24af726486", + strip_prefix = "TensorRT-10.0.0.6", urls = [ - "https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/secure/8.6.1/tars/TensorRT-8.6.1.6.Linux.x86_64-gnu.cuda-12.0.tar.gz", + "https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.0.0/tars/TensorRT-10.0.0.6.Linux.x86_64-gnu.cuda-12.4.tar.gz", ], ) From f0068c64ba4b5d214a3e1beb52bf15eaaae7cf21 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Wed, 27 Mar 2024 10:15:38 -0700 Subject: [PATCH 018/122] chore: update tensorrt version in CI --- dev_dep_versions.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev_dep_versions.yml b/dev_dep_versions.yml index 442485474c..b07db5533f 100644 --- a/dev_dep_versions.yml +++ b/dev_dep_versions.yml @@ -1,4 +1,4 @@ -__version__: "2.3.0.dev0" +__version__: "2.3.0" __cuda_version__: "12.1" __cudnn_version__: "8.9" -__tensorrt_version__: "8.6" +__tensorrt_version__: "10.0.0b6" From 39261b9d2d421496df5bd1d99b237aa459b9862f Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Wed, 27 Mar 2024 14:28:17 -0700 Subject: [PATCH 019/122] chore: more updates --- core/conversion/converters/impl/conv_deconv.cpp | 3 +-- core/conversion/converters/impl/interpolate.cpp | 13 ++++++++----- .../passes/unpack_scaled_dot_product_attention.cpp | 8 ++++---- .../test_scaled_dot_product_attention.cpp | 9 ++++++--- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/core/conversion/converters/impl/conv_deconv.cpp b/core/conversion/converters/impl/conv_deconv.cpp index a779a3c1b9..c71007ac03 100644 --- a/core/conversion/converters/impl/conv_deconv.cpp +++ b/core/conversion/converters/impl/conv_deconv.cpp @@ -293,8 +293,7 @@ bool add_conv_deconv(ConversionCtx* ctx, const torch::jit::Node* n, args& args) TORCHTRT_CHECK(conv, "Unable to create convolution layer from node: " << *n); conv->setStrideNd(stride); conv->setPaddingMode(nvinfer1::PaddingMode::kEXPLICIT_ROUND_DOWN); - conv->setPrePadding(padding); - conv->setPostPadding(padding); + conv->setPaddingNd(padding); conv->setDilationNd(dilation); conv->setNbGroups(groups); new_layer = conv; diff --git a/core/conversion/converters/impl/interpolate.cpp b/core/conversion/converters/impl/interpolate.cpp index 748e3c9e1a..64542d14f6 100644 --- a/core/conversion/converters/impl/interpolate.cpp +++ b/core/conversion/converters/impl/interpolate.cpp @@ -77,7 +77,6 @@ void resize_layer_size( TORCHTRT_CHECK((out_shape.size() > 0) ^ (scales.size() > 0), "only one of out_shape or scales should be defined"); auto resize_layer = ctx->net->addResize(*in); TORCHTRT_CHECK(resize_layer, "Unable to create interpolation (resizing) layer from node" << *n); - if (out_shape.size() > 0) { auto th_dynamic_shape_mask = torch::zeros(out_shape.size(), torch::kInt32); auto th_static_shape_mask = torch::zeros(out_shape.size(), torch::kInt32); @@ -108,13 +107,17 @@ void resize_layer_size( resize_layer->setResizeMode(mode); resize_layer->setName(util::node_info(n).c_str()); -#if NV_TENSORRT_MAJOR < 8 - resize_layer->setAlignCorners(align_corners); -#else + if (align_corners) { resize_layer->setCoordinateTransformation(nvinfer1::ResizeCoordinateTransformation::kALIGN_CORNERS); + } else { + if (mode == nvinfer1::InterpolationMode::kLINEAR) { + resize_layer->setCoordinateTransformation(nvinfer1::ResizeCoordinateTransformation::kHALF_PIXEL); + } else { + // kASYMMETRIC is the default transformation in TensorRT + resize_layer->setCoordinateTransformation(nvinfer1::ResizeCoordinateTransformation::kASYMMETRIC); + } } -#endif auto layer_output = ctx->AssociateValueAndTensor(n->outputs()[0], resize_layer->getOutput(0)); LOG_DEBUG("Output tensor shape: " << layer_output->getDimensions()); diff --git a/core/lowering/passes/unpack_scaled_dot_product_attention.cpp b/core/lowering/passes/unpack_scaled_dot_product_attention.cpp index bfe0004bd6..3c347f65ca 100644 --- a/core/lowering/passes/unpack_scaled_dot_product_attention.cpp +++ b/core/lowering/passes/unpack_scaled_dot_product_attention.cpp @@ -12,12 +12,12 @@ namespace passes { // https://pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html void UnpackScaledDotProductAttention(std::shared_ptr& graph) { std::string sdpa_pattern = R"IR( - graph(%query, %key, %value, %attn_mask, %dropout_p, %is_causal): - %out: Tensor = aten::scaled_dot_product_attention(%query, %key, %value, %attn_mask, %dropout_p, %is_causal) + graph(%query, %key, %value, %attn_mask, %dropout_p, %is_causal, %scale): + %out: Tensor = aten::scaled_dot_product_attention(%query, %key, %value, %attn_mask, %dropout_p, %is_causal, %scale) return (%out))IR"; std::string unpacked_sdpa_pattern = R"IR( - graph(%query, %key, %value, %attn_mask, %dropout_p, %is_causal): + graph(%query, %key, %value, %attn_mask, %dropout_p, %is_causal, %scale): %none : NoneType = prim::Constant() %1 : int = prim::Constant[value=-1]() %2 : int = prim::Constant[value=-2]() @@ -33,7 +33,7 @@ void UnpackScaledDotProductAttention(std::shared_ptr& graph) return(%out))IR"; std::string unpacked_sdpa_attn_biased_pattern = R"IR( - graph(%query, %key, %value, %attn_mask, %dropout_p, %is_causal): + graph(%query, %key, %value, %attn_mask, %dropout_p, %is_causal, %scale): %none : NoneType = prim::Constant() %0 : int = prim::Constant[value=1]() %1 : int = prim::Constant[value=-1]() diff --git a/tests/core/conversion/converters/test_scaled_dot_product_attention.cpp b/tests/core/conversion/converters/test_scaled_dot_product_attention.cpp index 785363ccca..7fa859e324 100644 --- a/tests/core/conversion/converters/test_scaled_dot_product_attention.cpp +++ b/tests/core/conversion/converters/test_scaled_dot_product_attention.cpp @@ -10,8 +10,9 @@ TEST(Converters, ATenScaledDotProductAttentionConvertsCorrectly) { graph(%query : Tensor, %key : Tensor, %value : Tensor): %none : NoneType = prim::Constant() %0 : float = prim::Constant[value=0.]() + %scale : float = prim::Constant[value=1.]() %false : bool = prim::Constant[value=0]() - %3 : Tensor = aten::scaled_dot_product_attention(%query, %key, %value, %none, %0, %false) + %3 : Tensor = aten::scaled_dot_product_attention(%query, %key, %value, %none, %0, %false, %scale) return (%3))IR"; auto g = std::make_shared(); @@ -36,7 +37,8 @@ TEST(Converters, ATenScaledDotProductAttnMaskFloatConvertsCorrectly) { graph(%query : Tensor, %key : Tensor, %value : Tensor, %attn_mask : Tensor): %0 : float = prim::Constant[value=0.]() %false : bool = prim::Constant[value=0]() - %3 : Tensor = aten::scaled_dot_product_attention(%query, %key, %value, %attn_mask, %0, %false) + %scale : float = prim::Constant[value=1.]() + %3 : Tensor = aten::scaled_dot_product_attention(%query, %key, %value, %attn_mask, %0, %false, %scale) return (%3))IR"; auto g = std::make_shared(); @@ -62,7 +64,8 @@ TEST(Converters, ATenScaledDotProductAttnMaskBoolConvertsCorrectly) { graph(%query : Tensor, %key : Tensor, %value : Tensor, %attn_mask : Tensor): %0 : float = prim::Constant[value=0.]() %false : bool = prim::Constant[value=0]() - %3 : Tensor = aten::scaled_dot_product_attention(%query, %key, %value, %attn_mask, %0, %false) + %scale : float = prim::Constant[value=1.]() + %3 : Tensor = aten::scaled_dot_product_attention(%query, %key, %value, %attn_mask, %0, %false, %scale) return (%3))IR"; auto g = std::make_shared(); From 3753150fb75e9a6637acffa35f2f3b4b63d15afa Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 2 Apr 2024 05:12:26 -0700 Subject: [PATCH 020/122] chore: more fixes --- .../test_scaled_dot_product_attention.cpp | 6 ++--- .../test_trt_intercompatibility.py | 24 ++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/tests/core/conversion/converters/test_scaled_dot_product_attention.cpp b/tests/core/conversion/converters/test_scaled_dot_product_attention.cpp index 7fa859e324..5550d5409b 100644 --- a/tests/core/conversion/converters/test_scaled_dot_product_attention.cpp +++ b/tests/core/conversion/converters/test_scaled_dot_product_attention.cpp @@ -10,7 +10,7 @@ TEST(Converters, ATenScaledDotProductAttentionConvertsCorrectly) { graph(%query : Tensor, %key : Tensor, %value : Tensor): %none : NoneType = prim::Constant() %0 : float = prim::Constant[value=0.]() - %scale : float = prim::Constant[value=1.]() + %scale : NoneType = prim::Constant() %false : bool = prim::Constant[value=0]() %3 : Tensor = aten::scaled_dot_product_attention(%query, %key, %value, %none, %0, %false, %scale) return (%3))IR"; @@ -37,7 +37,7 @@ TEST(Converters, ATenScaledDotProductAttnMaskFloatConvertsCorrectly) { graph(%query : Tensor, %key : Tensor, %value : Tensor, %attn_mask : Tensor): %0 : float = prim::Constant[value=0.]() %false : bool = prim::Constant[value=0]() - %scale : float = prim::Constant[value=1.]() + %scale : NoneType = prim::Constant() %3 : Tensor = aten::scaled_dot_product_attention(%query, %key, %value, %attn_mask, %0, %false, %scale) return (%3))IR"; @@ -64,7 +64,7 @@ TEST(Converters, ATenScaledDotProductAttnMaskBoolConvertsCorrectly) { graph(%query : Tensor, %key : Tensor, %value : Tensor, %attn_mask : Tensor): %0 : float = prim::Constant[value=0.]() %false : bool = prim::Constant[value=0]() - %scale : float = prim::Constant[value=1.]() + %scale : NoneType = prim::Constant() %3 : Tensor = aten::scaled_dot_product_attention(%query, %key, %value, %attn_mask, %0, %false, %scale) return (%3))IR"; diff --git a/tests/py/ts/integrations/test_trt_intercompatibility.py b/tests/py/ts/integrations/test_trt_intercompatibility.py index b938e4a1ac..4c84ad088f 100644 --- a/tests/py/ts/integrations/test_trt_intercompatibility.py +++ b/tests/py/ts/integrations/test_trt_intercompatibility.py @@ -1,9 +1,10 @@ import unittest -import torch_tensorrt as torchtrt + +import tensorrt as trt import torch +import torch_tensorrt as torchtrt import torchvision.models as models -import tensorrt as trt -from utils import cosine_similarity, COSINE_THRESHOLD +from utils import COSINE_THRESHOLD, cosine_similarity class TestPyTorchToTRTEngine(unittest.TestCase): @@ -31,18 +32,19 @@ def test_pt_to_trt(self): with trt.Runtime(TRT_LOGGER) as rt: engine = rt.deserialize_cuda_engine(trt_engine) with engine.create_execution_context() as ctx: - out = torch.empty(size=tuple(engine.get_binding_shape(1))).to("cuda:0") + out = torch.empty( + size=tuple(engine.get_tensor_shape(engine.get_tensor_name(1))) + ).to("cuda:0") bindings = [ self.input.contiguous().data_ptr(), out.contiguous().data_ptr(), ] - ctx.execute_async( - batch_size=1, - bindings=bindings, - stream_handle=torch.cuda.current_stream( - device="cuda:0" - ).cuda_stream, - ) + + # Assign tensor address appropriately + for idx in range(engine.num_io_tensors): + ctx.set_tensor_address(engine.get_tensor_name(idx), bindings[idx]) + ctx.execute_async_v3(torch.cuda.current_stream().cuda_stream) + cos_sim = cosine_similarity(self.model(self.input), out) self.assertTrue( cos_sim > COSINE_THRESHOLD, From c355766f659f15fe02bf373e004e10039047d58d Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 2 Apr 2024 05:54:57 -0700 Subject: [PATCH 021/122] chore: remove NvUtils.h --- third_party/tensorrt/local/BUILD | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/third_party/tensorrt/local/BUILD b/third_party/tensorrt/local/BUILD index 9cbe98a41e..59d0ef01d8 100644 --- a/third_party/tensorrt/local/BUILD +++ b/third_party/tensorrt/local/BUILD @@ -29,9 +29,7 @@ config_setting( cc_library( name = "nvinfer_headers", hdrs = select({ - ":aarch64_linux": [ - "include/aarch64-linux-gnu/NvUtils.h", - ] + glob( + ":aarch64_linux": glob( [ "include/aarch64-linux-gnu/NvInfer*.h", ], @@ -40,9 +38,7 @@ cc_library( "include/aarch64-linux-gnu/NvInferPluginUtils.h", ], ), - ":ci_rhel_x86_64_linux": [ - "include/NvUtils.h", - ] + glob( + ":ci_rhel_x86_64_linux": glob( [ "include/NvInfer*.h", ], @@ -51,9 +47,7 @@ cc_library( "include/NvInferPluginUtils.h", ], ), - ":windows": [ - "include/NvUtils.h", - ] + glob( + ":windows": glob( [ "include/NvInfer*.h", ], @@ -62,9 +56,7 @@ cc_library( "include/NvInferPluginUtils.h", ], ), - "//conditions:default": [ - "include/x86_64-linux-gnu/NvUtils.h", - ] + glob( + "//conditions:default": glob( [ "include/x86_64-linux-gnu/NvInfer*.h", ], From 2d237dca528d0743c4b748e60c7969de46660464 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 2 Apr 2024 07:14:43 -0700 Subject: [PATCH 022/122] chore: more updates --- WORKSPACE | 10 ---------- core/conversion/converters/impl/chunk.cpp | 4 ---- core/ir/ir.cpp | 1 - core/plugins/impl/interpolate_plugin.h | 1 - core/plugins/impl/normalize_plugin.h | 1 - third_party/tensorrt/local/BUILD | 2 -- .../ci_workspaces/WORKSPACE.x86_64.release.rhel.tmpl | 6 ------ 7 files changed, 25 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index 76e9a602fa..95abcea2d3 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -68,16 +68,6 @@ http_archive( # Either place them in the distdir directory in third_party and use the --distdir flag # or modify the urls to "file:////.tar.gz -http_archive( - name = "cudnn", - build_file = "@//third_party/cudnn/archive:BUILD", - sha256 = "2a2eb89a2ab51071151c6082f1e816c702167a711a9372f9f73a7b5c4b06e01a", - strip_prefix = "cudnn-linux-x86_64-8.9.5.30_cuda12-archive", - urls = [ - "https://developer.nvidia.com/downloads/compute/cudnn/secure/8.9.5/local_installers/12.x/cudnn-linux-x86_64-8.9.5.30_cuda12-archive.tar.xz", - ], -) - http_archive( name = "tensorrt", build_file = "@//third_party/tensorrt/archive:BUILD", diff --git a/core/conversion/converters/impl/chunk.cpp b/core/conversion/converters/impl/chunk.cpp index a7191133fb..b3d2441706 100644 --- a/core/conversion/converters/impl/chunk.cpp +++ b/core/conversion/converters/impl/chunk.cpp @@ -17,7 +17,6 @@ auto cat_registrations TORCHTRT_UNUSED = RegisterNodeConversionPatterns() auto chunks = args[1].unwrapToInt(); auto dim = args[2].unwrapToInt(); bool dynamic_shape = ctx->input_is_dynamic; - int size = in->getDimensions().nbDims; int maxDim = static_cast(in->getDimensions().d[dim]); c10::ListTypePtr lt = n->output()->type()->expect(); @@ -41,9 +40,6 @@ auto cat_registrations TORCHTRT_UNUSED = RegisterNodeConversionPatterns() size_.nbDims = nbdims; stride_.nbDims = nbdims; - int startIdx = 0; - int endIdx = maxDim; - for (int i = 0; i < nbdims; i++) { start_.d[i] = 0; size_.d[i] = 0; diff --git a/core/ir/ir.cpp b/core/ir/ir.cpp index c98d17c5ef..b67e228f1f 100644 --- a/core/ir/ir.cpp +++ b/core/ir/ir.cpp @@ -151,7 +151,6 @@ c10::optional get_value_first_calc_dtype_opt(torch::jit::Block* // If node outputs a Tensor it might be a result of tensor calcuation so check to see // if any inputs to the calculation can give us hints - c10::optional const_tensor_n = {}; // Backtrace to constants which will immediately give us the Tensor type if possible for (auto in : ins) { diff --git a/core/plugins/impl/interpolate_plugin.h b/core/plugins/impl/interpolate_plugin.h index ced4cbee20..ce009af03e 100644 --- a/core/plugins/impl/interpolate_plugin.h +++ b/core/plugins/impl/interpolate_plugin.h @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include diff --git a/core/plugins/impl/normalize_plugin.h b/core/plugins/impl/normalize_plugin.h index 28c3a5c5da..5d51a68293 100644 --- a/core/plugins/impl/normalize_plugin.h +++ b/core/plugins/impl/normalize_plugin.h @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include diff --git a/third_party/tensorrt/local/BUILD b/third_party/tensorrt/local/BUILD index 59d0ef01d8..c317e16688 100644 --- a/third_party/tensorrt/local/BUILD +++ b/third_party/tensorrt/local/BUILD @@ -104,7 +104,6 @@ cc_library( "nvinfer_headers", "nvinfer_lib", "@cuda//:cudart", - "@cudnn", ], ) @@ -358,7 +357,6 @@ cc_library( deps = [ "nvinfer", "@cuda//:cudart", - "@cudnn", ], alwayslink = True, ) diff --git a/toolchains/ci_workspaces/WORKSPACE.x86_64.release.rhel.tmpl b/toolchains/ci_workspaces/WORKSPACE.x86_64.release.rhel.tmpl index db6cfb0b5d..8ff4f74aaf 100644 --- a/toolchains/ci_workspaces/WORKSPACE.x86_64.release.rhel.tmpl +++ b/toolchains/ci_workspaces/WORKSPACE.x86_64.release.rhel.tmpl @@ -73,12 +73,6 @@ http_archive( # Locally installed dependencies (use in cases of custom dependencies or aarch64) #################################################################################### -new_local_repository( - name = "cudnn", - path = "/usr/", - build_file = "@//third_party/cudnn/local:BUILD" -) - new_local_repository( name = "tensorrt", path = "/usr/", From e4b442913b7200d66af92c90ebb54d77321bd536 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 2 Apr 2024 07:30:35 -0700 Subject: [PATCH 023/122] chore: change lib64 to lib in rhel BUILD file --- third_party/tensorrt/local/BUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/third_party/tensorrt/local/BUILD b/third_party/tensorrt/local/BUILD index c317e16688..1967308243 100644 --- a/third_party/tensorrt/local/BUILD +++ b/third_party/tensorrt/local/BUILD @@ -329,7 +329,7 @@ cc_library( name = "nvinferplugin", srcs = select({ ":aarch64_linux": ["lib/aarch64-linux-gnu/libnvinfer_plugin.so"], - ":ci_rhel_x86_64_linux": ["lib64/libnvinfer_plugin.so"], + ":ci_rhel_x86_64_linux": ["lib/libnvinfer_plugin.so"], ":windows": ["lib/nvinfer_plugin.dll"], "//conditions:default": ["lib/x86_64-linux-gnu/libnvinfer_plugin.so"], }), From fa4fb9cea4946d702e4cb869e76942fbaf4bcfda Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 2 Apr 2024 07:40:28 -0700 Subject: [PATCH 024/122] chore: more updates --- third_party/tensorrt/local/BUILD | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/third_party/tensorrt/local/BUILD b/third_party/tensorrt/local/BUILD index 1967308243..31aa906ba4 100644 --- a/third_party/tensorrt/local/BUILD +++ b/third_party/tensorrt/local/BUILD @@ -79,7 +79,7 @@ cc_import( name = "nvinfer_static_lib", static_library = select({ ":aarch64_linux": "lib/aarch64-linux-gnu/libnvinfer_static.a", - ":ci_rhel_x86_64_linux": "lib64/libnvinfer_static.a", + ":ci_rhel_x86_64_linux": "lib/libnvinfer_static.a", ":windows": "lib/nvinfer.lib", "//conditions:default": "lib/x86_64-linux-gnu/libnvinfer_static.a", }), @@ -90,7 +90,7 @@ cc_import( name = "nvinfer_lib", shared_library = select({ ":aarch64_linux": "lib/aarch64-linux-gnu/libnvinfer.so", - ":ci_rhel_x86_64_linux": "lib64/libnvinfer.so", + ":ci_rhel_x86_64_linux": "lib/libnvinfer.so", ":windows": "lib/nvinfer.dll", "//conditions:default": "lib/x86_64-linux-gnu/libnvinfer.so", }), @@ -113,7 +113,7 @@ cc_import( name = "nvparsers_lib", shared_library = select({ ":aarch64_linux": "lib/aarch64-linux-gnu/libnvparsers.so", - ":ci_rhel_x86_64_linux": "lib64/libnvparsers.so", + ":ci_rhel_x86_64_linux": "lib/libnvparsers.so", ":windows": "lib/nvparsers.dll", "//conditions:default": "lib/x86_64-linux-gnu/libnvparsers.so", }), @@ -177,7 +177,7 @@ cc_import( name = "nvonnxparser_lib", shared_library = select({ ":aarch64_linux": "lib/aarch64-linux-gnu/libnvonnxparser.so", - ":ci_rhel_x86_64_linux": "lib64/libnvonnxparser.so", + ":ci_rhel_x86_64_linux": "lib/libnvonnxparser.so", ":windows": "lib/nvonnxparser.dll", "//conditions:default": "lib/x86_64-linux-gnu/libnvonnxparser.so", }), @@ -233,7 +233,7 @@ cc_import( name = "nvonnxparser_runtime_lib", shared_library = select({ ":aarch64_linux": "lib/x86_64-linux-gnu/libnvonnxparser_runtime.so", - ":ci_rhel_x86_64_linux": "lib64/libnvonnxparser_runtime.so", + ":ci_rhel_x86_64_linux": "lib/libnvonnxparser_runtime.so", ":windows": "lib/nvonnxparser_runtime.dll", "//conditions:default": "lib/x86_64-linux-gnu/libnvonnxparser_runtime.so", }), @@ -281,7 +281,7 @@ cc_import( name = "nvcaffeparser_lib", shared_library = select({ ":aarch64_linux": "lib/aarch64-linux-gnu/libnvcaffe_parsers.so", - ":ci_rhel_x86_64_linux": "lib64/libnvcaffe_parsers.so", + ":ci_rhel_x86_64_linux": "lib/libnvcaffe_parsers.so", ":windows": "lib/nvcaffe_parsers.dll", "//conditions:default": "lib/x86_64-linux-gnu/libnvcaffe_parsers.so", }), From e11eb60edd8718c775f23b753d658a43f83edbe5 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 2 Apr 2024 09:36:52 -0700 Subject: [PATCH 025/122] chore: fix TRT version --- dev_dep_versions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev_dep_versions.yml b/dev_dep_versions.yml index b07db5533f..e2434d0c45 100644 --- a/dev_dep_versions.yml +++ b/dev_dep_versions.yml @@ -1,4 +1,4 @@ __version__: "2.3.0" __cuda_version__: "12.1" __cudnn_version__: "8.9" -__tensorrt_version__: "10.0.0b6" +__tensorrt_version__: "10.0" From 092feb225e8844d94c5e7b11d65b5e413cf88c48 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 2 Apr 2024 09:50:03 -0700 Subject: [PATCH 026/122] chore: more updates --- dev_dep_versions.yml | 2 +- packaging/pre_build_script.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev_dep_versions.yml b/dev_dep_versions.yml index e2434d0c45..b07db5533f 100644 --- a/dev_dep_versions.yml +++ b/dev_dep_versions.yml @@ -1,4 +1,4 @@ __version__: "2.3.0" __cuda_version__: "12.1" __cudnn_version__: "8.9" -__tensorrt_version__: "10.0" +__tensorrt_version__: "10.0.0b6" diff --git a/packaging/pre_build_script.sh b/packaging/pre_build_script.sh index 18cd5d9fe2..9bab9a58c3 100755 --- a/packaging/pre_build_script.sh +++ b/packaging/pre_build_script.sh @@ -5,7 +5,7 @@ python3 -m pip install pyyaml TRT_VERSION=$(python3 -c "import versions; versions.tensorrt_version()") yum-config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel7/x86_64/cuda-rhel7.repo yum check-update -yum install -y ninja-build gettext tensorrt-${TRT_VERSION}.* +yum install -y ninja-build gettext tensorrt-${TRT_VERSION} wget https://github.com/bazelbuild/bazelisk/releases/download/v1.17.0/bazelisk-linux-amd64 \ && mv bazelisk-linux-amd64 /usr/bin/bazel \ && chmod +x /usr/bin/bazel From 09ecf263741fb0e945033f602c424a2e5aabb4ad Mon Sep 17 00:00:00 2001 From: Evan Li Date: Wed, 3 Apr 2024 00:32:54 +0000 Subject: [PATCH 027/122] fix shape bug in bitwise ops --- .../conversion/impl/elementwise/base.py | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/py/torch_tensorrt/dynamo/conversion/impl/elementwise/base.py b/py/torch_tensorrt/dynamo/conversion/impl/elementwise/base.py index 615cd0bd62..f93ec52504 100644 --- a/py/torch_tensorrt/dynamo/conversion/impl/elementwise/base.py +++ b/py/torch_tensorrt/dynamo/conversion/impl/elementwise/base.py @@ -7,12 +7,13 @@ import torch from torch.fx.node import Target from torch_tensorrt.dynamo._SourceIR import SourceIR +from torch_tensorrt.dynamo.conversion import impl from torch_tensorrt.dynamo.conversion._ConversionContext import ConversionContext from torch_tensorrt.dynamo.conversion.converter_utils import ( cast_trt_tensor, get_trt_tensor, ) -from torch_tensorrt.fx.converters.converter_utils import broadcast, set_layer_name +from torch_tensorrt.fx.converters.converter_utils import set_layer_name from torch_tensorrt.fx.types import TRTElementWiseOp, TRTTensor from torch_tensorrt.fx.utils import Frameworks, unified_dtype_converter @@ -140,16 +141,45 @@ def convert_binary_elementwise( if trt_promoted_type != lhs_val.dtype: lhs_val = cast_trt_tensor( - ctx, lhs_val, trt_promoted_type, name, target, source_ir + ctx, lhs_val, trt_promoted_type, f"{name}_cast_lhs_val", target, source_ir ) if trt_promoted_type != rhs_val.dtype: rhs_val = cast_trt_tensor( - ctx, rhs_val, trt_promoted_type, name, target, source_ir + ctx, rhs_val, trt_promoted_type, f"{name}_cast_rhs_val", target, source_ir ) - lhs_val, rhs_val = broadcast( - ctx.net, lhs_val, rhs_val, f"{name}_lhs", f"{name}_rhs" - ) + lhs_val_shape = lhs_val.shape + rhs_val_shape = rhs_val.shape + rank_diff = len(lhs_val_shape) - len(rhs_val_shape) + if rank_diff > 0: + rhs_val = impl.slice.expand( + ctx, target, source_ir, f"{name}_expand_rhs_val", rhs_val, lhs_val_shape + ) + elif rank_diff < 0: + lhs_val = impl.slice.expand( + ctx, target, source_ir, f"{name}_expand_lhs_val", lhs_val, rhs_val_shape + ) + else: + if tuple(lhs_val_shape) != tuple(rhs_val_shape): + sum_diff = sum(lhs_val_shape) - sum(rhs_val_shape) + if sum_diff > 0: + rhs_val = impl.slice.expand( + ctx, + target, + source_ir, + f"{name}_expand_rhs_val", + rhs_val, + lhs_val_shape, + ) + elif sum_diff < 0: + lhs_val = impl.slice.expand( + ctx, + target, + source_ir, + f"{name}_expand_lhs_val", + lhs_val, + rhs_val_shape, + ) layer = ctx.net.add_elementwise(lhs_val, rhs_val, op_type) set_layer_name(layer, target, name, source_ir) From 85e04c5d5697e60395b79829cf80322da1de41f9 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 2 Apr 2024 19:07:46 -0700 Subject: [PATCH 028/122] chore: update to rhel9 --- packaging/pre_build_script.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/pre_build_script.sh b/packaging/pre_build_script.sh index 9bab9a58c3..ae73d2b773 100755 --- a/packaging/pre_build_script.sh +++ b/packaging/pre_build_script.sh @@ -3,7 +3,7 @@ # Install dependencies python3 -m pip install pyyaml TRT_VERSION=$(python3 -c "import versions; versions.tensorrt_version()") -yum-config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel7/x86_64/cuda-rhel7.repo +yum-config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel9/x86_64/cuda-rhel9.repo yum check-update yum install -y ninja-build gettext tensorrt-${TRT_VERSION} wget https://github.com/bazelbuild/bazelisk/releases/download/v1.17.0/bazelisk-linux-amd64 \ From 41229d63ae3465304f451a0b43703ea9754e0d97 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 2 Apr 2024 19:26:52 -0700 Subject: [PATCH 029/122] chore: change trt version --- dev_dep_versions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev_dep_versions.yml b/dev_dep_versions.yml index b07db5533f..4bbfe9d188 100644 --- a/dev_dep_versions.yml +++ b/dev_dep_versions.yml @@ -1,4 +1,4 @@ __version__: "2.3.0" __cuda_version__: "12.1" __cudnn_version__: "8.9" -__tensorrt_version__: "10.0.0b6" +__tensorrt_version__: "10.0.0.6" From 9d7a65618a039802eb568264eccf1be4172a3750 Mon Sep 17 00:00:00 2001 From: Evan Li Date: Wed, 3 Apr 2024 18:23:40 +0000 Subject: [PATCH 030/122] fix test bug and add more tests --- tests/py/dynamo/conversion/test_arange_aten.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/py/dynamo/conversion/test_arange_aten.py b/tests/py/dynamo/conversion/test_arange_aten.py index 035b957865..e06239eb4e 100644 --- a/tests/py/dynamo/conversion/test_arange_aten.py +++ b/tests/py/dynamo/conversion/test_arange_aten.py @@ -15,14 +15,18 @@ class TestArangeConverter(DispatchTestCase): (5, 0, -1), (5, 1, -2), (5, 3, -3), + (5, -2, -1), + (-5, -2, 2), + (-5, -3, 1), + (-2, -5, -1), ] ) def test_arange(self, start, end, step): class Arange(nn.Module): def forward(self, x): - return torch.ops.aten.arange.start_step(start, x.shape[0], step) + return torch.ops.aten.arange.start_step(start, end, step) - inputs = [torch.randn(end, 1)] + inputs = [torch.randn(1, 1)] self.run_test( Arange(), inputs, From 5e911a9765c4aad015a6075afd4b30bcca1efa79 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Wed, 3 Apr 2024 11:35:30 -0700 Subject: [PATCH 031/122] chore: delete mirror of rules_pkg --- WORKSPACE | 1 - third_party/tensorrt/archive/BUILD | 2 -- 2 files changed, 3 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index 95abcea2d3..13c07a6008 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -17,7 +17,6 @@ http_archive( name = "rules_pkg", sha256 = "8f9ee2dc10c1ae514ee599a8b42ed99fa262b757058f65ad3c384289ff70c4b8", urls = [ - "https://mirror.bazel.build/github.com/bazelbuild/rules_pkg/releases/download/0.9.1/rules_pkg-0.9.1.tar.gz", "https://github.com/bazelbuild/rules_pkg/releases/download/0.9.1/rules_pkg-0.9.1.tar.gz", ], ) diff --git a/third_party/tensorrt/archive/BUILD b/third_party/tensorrt/archive/BUILD index 221f2ce4b3..5c07794a20 100644 --- a/third_party/tensorrt/archive/BUILD +++ b/third_party/tensorrt/archive/BUILD @@ -45,7 +45,6 @@ cc_library( "nvinfer_headers", "nvinfer_lib", "@cuda//:cudart", - "@cudnn", ], ) @@ -182,6 +181,5 @@ cc_library( "nvinferplugin_headers", "nvinferplugin_lib", "@cuda//:cudart", - "@cudnn", ], ) From dae0eb2321418e575c826231ce2d2e5194837f06 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Wed, 3 Apr 2024 16:10:12 -0700 Subject: [PATCH 032/122] chore: fix conv test --- tests/core/conversion/converters/test_conv_deconv.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/core/conversion/converters/test_conv_deconv.cpp b/tests/core/conversion/converters/test_conv_deconv.cpp index 27baa1df5e..faaf7f2474 100644 --- a/tests/core/conversion/converters/test_conv_deconv.cpp +++ b/tests/core/conversion/converters/test_conv_deconv.cpp @@ -126,13 +126,13 @@ TEST(Converters, ATenConv1dWithWeightTensorsConvertsCorrectly) { %5 : int = prim::Constant[value=127]() %quant_input : Tensor = aten::fake_quantize_per_tensor_affine(%0, %3, %4, %2, %5) %6 : int = prim::Constant[value=6]() - %7 : int = prim::Constant[value=5]() + %7 : int = prim::Constant[value=4]() %8 : Device = prim::Constant[value="cuda:0"]() %9 : None = prim::Constant() %10 : int[] = prim::ListConstruct(%7) %11 : Tensor = aten::full(%10, %3, %6, %9, %8, %9) %12 : int[] = prim::ListConstruct(%7) - %13 : int = prim::Constant[value=1]() + %13 : int = prim::Constant[value=0]() %14 : Tensor = aten::full(%12, %13, %6, %9, %8, %9) %quant_wts : Tensor = aten::fake_quantize_per_channel_affine(%1, %11, %14, %13, %2, %5) %15 : None = prim::Constant() From 4676cd2d289f23870aa0b1e6829ad498e6855146 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Wed, 3 Apr 2024 17:06:25 -0700 Subject: [PATCH 033/122] chore: fix trt version range --- py/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/requirements.txt b/py/requirements.txt index 565395b59b..19c39c8647 100644 --- a/py/requirements.txt +++ b/py/requirements.txt @@ -5,5 +5,5 @@ pybind11==2.6.2 torch==2.3.0 torchvision==0.18.0 --extra-index-url https://pypi.ngc.nvidia.com -tensorrt==10.0.0b6 +tensorrt>=10.0 pyyaml From 88efe8e7258c4af254299c7ba37f4af4176cba5b Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Wed, 3 Apr 2024 17:10:04 -0700 Subject: [PATCH 034/122] chore: fix trt rangfe --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7468484f48..05404f87b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ requires = [ "cffi>=1.15.1", "typing-extensions>=4.7.0", "future>=0.18.3", - "tensorrt==10.0.0b6", + "tensorrt>=10.0", "torch >=2.3.0.dev,<2.4.0", "pybind11==2.6.2", "numpy", From f9b40e672da686591830b7a1bef1280231af6a2b Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Wed, 3 Apr 2024 17:18:01 -0700 Subject: [PATCH 035/122] chore: minor fix --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 05404f87b7..7a78047be7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ requires = [ "typing-extensions>=4.7.0", "future>=0.18.3", "tensorrt>=10.0", - "torch >=2.3.0.dev,<2.4.0", + "torch>=2.3.0.dev,<2.4.0", "pybind11==2.6.2", "numpy", ] @@ -42,7 +42,7 @@ requires-python = ">=3.8" keywords = ["pytorch", "torch", "tensorrt", "trt", "ai", "artificial intelligence", "ml", "machine learning", "dl", "deep learning", "compiler", "dynamo", "torchscript", "inference"] dependencies = [ "torch >=2.3.0.dev,<2.4.0", - "tensorrt==10.0.0b6", + "tensorrt>=10.0", "packaging>=23", "numpy", "typing-extensions>=4.7.0", From b86aec27a3c8932d94222ca970d0f4178f742841 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Thu, 4 Apr 2024 10:26:46 -0700 Subject: [PATCH 036/122] chore: update rules_pkg --- WORKSPACE | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index 13c07a6008..4a7a1556d3 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -15,9 +15,9 @@ py_repositories() http_archive( name = "rules_pkg", - sha256 = "8f9ee2dc10c1ae514ee599a8b42ed99fa262b757058f65ad3c384289ff70c4b8", + sha256 = "d250924a2ecc5176808fc4c25d5cf5e9e79e6346d79d5ab1c493e289e722d1d0", urls = [ - "https://github.com/bazelbuild/rules_pkg/releases/download/0.9.1/rules_pkg-0.9.1.tar.gz", + "https://github.com/bazelbuild/rules_pkg/releases/download/0.10.1/rules_pkg-0.10.1.tar.gz", ], ) From 6630281671d52adb765e4d816a8d5315bfbbc6ce Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Thu, 4 Apr 2024 13:16:53 -0700 Subject: [PATCH 037/122] chore: minor fixes --- WORKSPACE | 4 ++-- py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index 4a7a1556d3..13c07a6008 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -15,9 +15,9 @@ py_repositories() http_archive( name = "rules_pkg", - sha256 = "d250924a2ecc5176808fc4c25d5cf5e9e79e6346d79d5ab1c493e289e722d1d0", + sha256 = "8f9ee2dc10c1ae514ee599a8b42ed99fa262b757058f65ad3c384289ff70c4b8", urls = [ - "https://github.com/bazelbuild/rules_pkg/releases/download/0.10.1/rules_pkg-0.10.1.tar.gz", + "https://github.com/bazelbuild/rules_pkg/releases/download/0.9.1/rules_pkg-0.9.1.tar.gz", ], ) diff --git a/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py b/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py index ffcc9c195e..a1f6614c85 100644 --- a/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py +++ b/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py @@ -188,6 +188,7 @@ def run( if self.compilation_settings.version_compatible: _LOGGER.info("Using version compatible") builder_config.set_flag(trt.BuilderFlag.VERSION_COMPATIBLE) + builder_config.set_flag(trt.BuilderFlag.EXCLUDE_LEAN_RUNTIME) if self.compilation_settings.hardware_compatible: _LOGGER.info("Using hardware compatible") builder_config.hardware_compatibility_level = ( From fca55fe06bcba9d9282f275a2aa713f94a73b500 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Thu, 4 Apr 2024 14:22:06 -0700 Subject: [PATCH 038/122] chore: expt --- WORKSPACE | 17 +++++++++++++--- third_party/tensorrt/archive/BUILD | 2 ++ third_party/tensorrt/local/BUILD | 32 ++++++++++++++++++++---------- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index 13c07a6008..edc5c9a050 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -17,6 +17,7 @@ http_archive( name = "rules_pkg", sha256 = "8f9ee2dc10c1ae514ee599a8b42ed99fa262b757058f65ad3c384289ff70c4b8", urls = [ + "https://mirror.bazel.build/github.com/bazelbuild/rules_pkg/releases/download/0.9.1/rules_pkg-0.9.1.tar.gz", "https://github.com/bazelbuild/rules_pkg/releases/download/0.9.1/rules_pkg-0.9.1.tar.gz", ], ) @@ -67,13 +68,23 @@ http_archive( # Either place them in the distdir directory in third_party and use the --distdir flag # or modify the urls to "file:////.tar.gz +http_archive( + name = "cudnn", + build_file = "@//third_party/cudnn/archive:BUILD", + sha256 = "2a2eb89a2ab51071151c6082f1e816c702167a711a9372f9f73a7b5c4b06e01a", + strip_prefix = "cudnn-linux-x86_64-8.9.5.30_cuda12-archive", + urls = [ + "https://developer.nvidia.com/downloads/compute/cudnn/secure/8.9.5/local_installers/12.x/cudnn-linux-x86_64-8.9.5.30_cuda12-archive.tar.xz", + ], +) + http_archive( name = "tensorrt", build_file = "@//third_party/tensorrt/archive:BUILD", - sha256 = "7ad5c8eab8fe8803bef5d8cee54ec1f407797ac8edb8ea3186cb9f24af726486", - strip_prefix = "TensorRT-10.0.0.6", + sha256 = "0f8157a5fc5329943b338b893591373350afa90ca81239cdadd7580cd1eba254", + strip_prefix = "TensorRT-8.6.1.6", urls = [ - "https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.0.0/tars/TensorRT-10.0.0.6.Linux.x86_64-gnu.cuda-12.4.tar.gz", + "https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/secure/8.6.1/tars/TensorRT-8.6.1.6.Linux.x86_64-gnu.cuda-12.0.tar.gz", ], ) diff --git a/third_party/tensorrt/archive/BUILD b/third_party/tensorrt/archive/BUILD index 5c07794a20..221f2ce4b3 100644 --- a/third_party/tensorrt/archive/BUILD +++ b/third_party/tensorrt/archive/BUILD @@ -45,6 +45,7 @@ cc_library( "nvinfer_headers", "nvinfer_lib", "@cuda//:cudart", + "@cudnn", ], ) @@ -181,5 +182,6 @@ cc_library( "nvinferplugin_headers", "nvinferplugin_lib", "@cuda//:cudart", + "@cudnn", ], ) diff --git a/third_party/tensorrt/local/BUILD b/third_party/tensorrt/local/BUILD index 31aa906ba4..9cbe98a41e 100644 --- a/third_party/tensorrt/local/BUILD +++ b/third_party/tensorrt/local/BUILD @@ -29,7 +29,9 @@ config_setting( cc_library( name = "nvinfer_headers", hdrs = select({ - ":aarch64_linux": glob( + ":aarch64_linux": [ + "include/aarch64-linux-gnu/NvUtils.h", + ] + glob( [ "include/aarch64-linux-gnu/NvInfer*.h", ], @@ -38,7 +40,9 @@ cc_library( "include/aarch64-linux-gnu/NvInferPluginUtils.h", ], ), - ":ci_rhel_x86_64_linux": glob( + ":ci_rhel_x86_64_linux": [ + "include/NvUtils.h", + ] + glob( [ "include/NvInfer*.h", ], @@ -47,7 +51,9 @@ cc_library( "include/NvInferPluginUtils.h", ], ), - ":windows": glob( + ":windows": [ + "include/NvUtils.h", + ] + glob( [ "include/NvInfer*.h", ], @@ -56,7 +62,9 @@ cc_library( "include/NvInferPluginUtils.h", ], ), - "//conditions:default": glob( + "//conditions:default": [ + "include/x86_64-linux-gnu/NvUtils.h", + ] + glob( [ "include/x86_64-linux-gnu/NvInfer*.h", ], @@ -79,7 +87,7 @@ cc_import( name = "nvinfer_static_lib", static_library = select({ ":aarch64_linux": "lib/aarch64-linux-gnu/libnvinfer_static.a", - ":ci_rhel_x86_64_linux": "lib/libnvinfer_static.a", + ":ci_rhel_x86_64_linux": "lib64/libnvinfer_static.a", ":windows": "lib/nvinfer.lib", "//conditions:default": "lib/x86_64-linux-gnu/libnvinfer_static.a", }), @@ -90,7 +98,7 @@ cc_import( name = "nvinfer_lib", shared_library = select({ ":aarch64_linux": "lib/aarch64-linux-gnu/libnvinfer.so", - ":ci_rhel_x86_64_linux": "lib/libnvinfer.so", + ":ci_rhel_x86_64_linux": "lib64/libnvinfer.so", ":windows": "lib/nvinfer.dll", "//conditions:default": "lib/x86_64-linux-gnu/libnvinfer.so", }), @@ -104,6 +112,7 @@ cc_library( "nvinfer_headers", "nvinfer_lib", "@cuda//:cudart", + "@cudnn", ], ) @@ -113,7 +122,7 @@ cc_import( name = "nvparsers_lib", shared_library = select({ ":aarch64_linux": "lib/aarch64-linux-gnu/libnvparsers.so", - ":ci_rhel_x86_64_linux": "lib/libnvparsers.so", + ":ci_rhel_x86_64_linux": "lib64/libnvparsers.so", ":windows": "lib/nvparsers.dll", "//conditions:default": "lib/x86_64-linux-gnu/libnvparsers.so", }), @@ -177,7 +186,7 @@ cc_import( name = "nvonnxparser_lib", shared_library = select({ ":aarch64_linux": "lib/aarch64-linux-gnu/libnvonnxparser.so", - ":ci_rhel_x86_64_linux": "lib/libnvonnxparser.so", + ":ci_rhel_x86_64_linux": "lib64/libnvonnxparser.so", ":windows": "lib/nvonnxparser.dll", "//conditions:default": "lib/x86_64-linux-gnu/libnvonnxparser.so", }), @@ -233,7 +242,7 @@ cc_import( name = "nvonnxparser_runtime_lib", shared_library = select({ ":aarch64_linux": "lib/x86_64-linux-gnu/libnvonnxparser_runtime.so", - ":ci_rhel_x86_64_linux": "lib/libnvonnxparser_runtime.so", + ":ci_rhel_x86_64_linux": "lib64/libnvonnxparser_runtime.so", ":windows": "lib/nvonnxparser_runtime.dll", "//conditions:default": "lib/x86_64-linux-gnu/libnvonnxparser_runtime.so", }), @@ -281,7 +290,7 @@ cc_import( name = "nvcaffeparser_lib", shared_library = select({ ":aarch64_linux": "lib/aarch64-linux-gnu/libnvcaffe_parsers.so", - ":ci_rhel_x86_64_linux": "lib/libnvcaffe_parsers.so", + ":ci_rhel_x86_64_linux": "lib64/libnvcaffe_parsers.so", ":windows": "lib/nvcaffe_parsers.dll", "//conditions:default": "lib/x86_64-linux-gnu/libnvcaffe_parsers.so", }), @@ -329,7 +338,7 @@ cc_library( name = "nvinferplugin", srcs = select({ ":aarch64_linux": ["lib/aarch64-linux-gnu/libnvinfer_plugin.so"], - ":ci_rhel_x86_64_linux": ["lib/libnvinfer_plugin.so"], + ":ci_rhel_x86_64_linux": ["lib64/libnvinfer_plugin.so"], ":windows": ["lib/nvinfer_plugin.dll"], "//conditions:default": ["lib/x86_64-linux-gnu/libnvinfer_plugin.so"], }), @@ -357,6 +366,7 @@ cc_library( deps = [ "nvinfer", "@cuda//:cudart", + "@cudnn", ], alwayslink = True, ) From 1ca01e782915a8073f546e53b9f171e9675e4731 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Thu, 4 Apr 2024 18:19:23 -0700 Subject: [PATCH 039/122] chore: update WORKSPACE tmpl --- toolchains/ci_workspaces/WORKSPACE.x86_64.release.rhel.tmpl | 1 - 1 file changed, 1 deletion(-) diff --git a/toolchains/ci_workspaces/WORKSPACE.x86_64.release.rhel.tmpl b/toolchains/ci_workspaces/WORKSPACE.x86_64.release.rhel.tmpl index 8ff4f74aaf..4a37bb6091 100644 --- a/toolchains/ci_workspaces/WORKSPACE.x86_64.release.rhel.tmpl +++ b/toolchains/ci_workspaces/WORKSPACE.x86_64.release.rhel.tmpl @@ -17,7 +17,6 @@ http_archive( name = "rules_pkg", sha256 = "8f9ee2dc10c1ae514ee599a8b42ed99fa262b757058f65ad3c384289ff70c4b8", urls = [ - "https://mirror.bazel.build/github.com/bazelbuild/rules_pkg/releases/download/0.9.1/rules_pkg-0.9.1.tar.gz", "https://github.com/bazelbuild/rules_pkg/releases/download/0.9.1/rules_pkg-0.9.1.tar.gz", ], ) From 6ffb85ee051604aa5c57d8c8802bc47525fcc22d Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Fri, 5 Apr 2024 16:50:34 -0700 Subject: [PATCH 040/122] chore: fix --- packaging/pre_build_script.sh | 3 ++- pyproject.toml | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packaging/pre_build_script.sh b/packaging/pre_build_script.sh index ae73d2b773..41fb47ff79 100755 --- a/packaging/pre_build_script.sh +++ b/packaging/pre_build_script.sh @@ -2,10 +2,11 @@ # Install dependencies python3 -m pip install pyyaml +yum install -y ninja-build gettext TRT_VERSION=$(python3 -c "import versions; versions.tensorrt_version()") yum-config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel9/x86_64/cuda-rhel9.repo yum check-update -yum install -y ninja-build gettext tensorrt-${TRT_VERSION} +yum install tensorrt-${TRT_VERSION} wget https://github.com/bazelbuild/bazelisk/releases/download/v1.17.0/bazelisk-linux-amd64 \ && mv bazelisk-linux-amd64 /usr/bin/bazel \ && chmod +x /usr/bin/bazel diff --git a/pyproject.toml b/pyproject.toml index 1e29a43565..dd4cada7f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ requires = [ "cffi>=1.15.1", "typing-extensions>=4.7.0", "future>=0.18.3", - "tensorrt>=8.6,<8.7", + "tensorrt==10.0.0b6", "torch==2.3.0", "pybind11==2.6.2", "numpy", @@ -42,7 +42,7 @@ requires-python = ">=3.8" keywords = ["pytorch", "torch", "tensorrt", "trt", "ai", "artificial intelligence", "ml", "machine learning", "dl", "deep learning", "compiler", "dynamo", "torchscript", "inference"] dependencies = [ "torch==2.3.0", - "tensorrt>=8.6,<8.7", + "tensorrt==10.0.0b6", "packaging>=23", "numpy", "typing-extensions>=4.7.0", From 76af510ebb7129460d70b759677673878d767c01 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Fri, 5 Apr 2024 17:05:18 -0700 Subject: [PATCH 041/122] chore: remove cudnn dep --- third_party/tensorrt/archive/BUILD | 2 -- third_party/tensorrt/local/BUILD | 2 -- 2 files changed, 4 deletions(-) diff --git a/third_party/tensorrt/archive/BUILD b/third_party/tensorrt/archive/BUILD index 221f2ce4b3..5c07794a20 100644 --- a/third_party/tensorrt/archive/BUILD +++ b/third_party/tensorrt/archive/BUILD @@ -45,7 +45,6 @@ cc_library( "nvinfer_headers", "nvinfer_lib", "@cuda//:cudart", - "@cudnn", ], ) @@ -182,6 +181,5 @@ cc_library( "nvinferplugin_headers", "nvinferplugin_lib", "@cuda//:cudart", - "@cudnn", ], ) diff --git a/third_party/tensorrt/local/BUILD b/third_party/tensorrt/local/BUILD index 9cbe98a41e..5136563521 100644 --- a/third_party/tensorrt/local/BUILD +++ b/third_party/tensorrt/local/BUILD @@ -112,7 +112,6 @@ cc_library( "nvinfer_headers", "nvinfer_lib", "@cuda//:cudart", - "@cudnn", ], ) @@ -366,7 +365,6 @@ cc_library( deps = [ "nvinfer", "@cuda//:cudart", - "@cudnn", ], alwayslink = True, ) From f9cf75ae8b7c91d88195b182fda784fb6fcce792 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Fri, 5 Apr 2024 17:20:35 -0700 Subject: [PATCH 042/122] chore: fix --- third_party/tensorrt/local/BUILD | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/third_party/tensorrt/local/BUILD b/third_party/tensorrt/local/BUILD index 5136563521..c317e16688 100644 --- a/third_party/tensorrt/local/BUILD +++ b/third_party/tensorrt/local/BUILD @@ -29,9 +29,7 @@ config_setting( cc_library( name = "nvinfer_headers", hdrs = select({ - ":aarch64_linux": [ - "include/aarch64-linux-gnu/NvUtils.h", - ] + glob( + ":aarch64_linux": glob( [ "include/aarch64-linux-gnu/NvInfer*.h", ], @@ -40,9 +38,7 @@ cc_library( "include/aarch64-linux-gnu/NvInferPluginUtils.h", ], ), - ":ci_rhel_x86_64_linux": [ - "include/NvUtils.h", - ] + glob( + ":ci_rhel_x86_64_linux": glob( [ "include/NvInfer*.h", ], @@ -51,9 +47,7 @@ cc_library( "include/NvInferPluginUtils.h", ], ), - ":windows": [ - "include/NvUtils.h", - ] + glob( + ":windows": glob( [ "include/NvInfer*.h", ], @@ -62,9 +56,7 @@ cc_library( "include/NvInferPluginUtils.h", ], ), - "//conditions:default": [ - "include/x86_64-linux-gnu/NvUtils.h", - ] + glob( + "//conditions:default": glob( [ "include/x86_64-linux-gnu/NvInfer*.h", ], From 33ba8b2c619b57f151d6eba74dfc885f8af03d2a Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 8 Apr 2024 14:27:27 -0700 Subject: [PATCH 043/122] chore: updates --- packaging/pre_build_script.sh | 6 +++--- .../WORKSPACE.x86_64.release.rhel.tmpl | 13 +++++-------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/packaging/pre_build_script.sh b/packaging/pre_build_script.sh index 41fb47ff79..5905e44d2e 100755 --- a/packaging/pre_build_script.sh +++ b/packaging/pre_build_script.sh @@ -4,9 +4,9 @@ python3 -m pip install pyyaml yum install -y ninja-build gettext TRT_VERSION=$(python3 -c "import versions; versions.tensorrt_version()") -yum-config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel9/x86_64/cuda-rhel9.repo -yum check-update -yum install tensorrt-${TRT_VERSION} +wget -P /opt/torch-tensorrt-builds/ https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.0.0/TensorRT-10.0.0.6.Linux.x86_64-gnu.cuda-12.4.tar.gz +tar -xvzf /opt/torch-tensorrt-builds/TensorRT-10.0.0.6.Linux.x86_64-gnu.cuda-12.4.tar.gz -C /opt/torch-tensorrt-builds/ +export LD_LIBRARY_PATH=/opt/torch-tensorrt-builds/TensorRT-10.0.0.6/lib:$LD_LIBRARY_PATH wget https://github.com/bazelbuild/bazelisk/releases/download/v1.17.0/bazelisk-linux-amd64 \ && mv bazelisk-linux-amd64 /usr/bin/bazel \ && chmod +x /usr/bin/bazel diff --git a/toolchains/ci_workspaces/WORKSPACE.x86_64.release.rhel.tmpl b/toolchains/ci_workspaces/WORKSPACE.x86_64.release.rhel.tmpl index 4a37bb6091..cad54b1707 100644 --- a/toolchains/ci_workspaces/WORKSPACE.x86_64.release.rhel.tmpl +++ b/toolchains/ci_workspaces/WORKSPACE.x86_64.release.rhel.tmpl @@ -68,14 +68,11 @@ http_archive( urls = ["https://download.pytorch.org/libtorch/test/cu121/libtorch-shared-with-deps-2.3.0%2Bcu121.zip"], ) -#################################################################################### -# Locally installed dependencies (use in cases of custom dependencies or aarch64) -#################################################################################### - -new_local_repository( - name = "tensorrt", - path = "/usr/", - build_file = "@//third_party/tensorrt/local:BUILD" +http_archive( + name = "tensorrt", + urls = ["file:////opt/torch-tensorrt-builds/TensorRT-10.0.0.6.Linux.x86_64-gnu.cuda-12.4.tar.gz",], + build_file = "@//third_party/tensorrt/archive:BUILD", + strip_prefix = "TensorRT-10.0.0.6" ) # ######################################################################### From 923377c86e5a664ac13c992188c91748fcc44b07 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 8 Apr 2024 21:44:50 -0700 Subject: [PATCH 044/122] chore: update post-build script --- .github/workflows/build-test.yml | 8 +++++++- packaging/post_build_script.sh | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 5573bb8d28..febc475c09 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -36,7 +36,7 @@ jobs: - repository: pytorch/tensorrt pre-script: packaging/pre_build_script.sh env-var-script: packaging/env_vars.txt - post-script: "" + post-script: packaging/post_build_script.sh smoke-test-script: "" package-name: torch_tensorrt name: Build torch-tensorrt whl package @@ -64,6 +64,7 @@ jobs: - repository: pytorch/tensorrt package-name: torch_tensorrt pre-script: packaging/pre_build_script.sh + post-script: packaging/post_build_script.sh uses: pytorch/tensorrt/.github/workflows/linux-test.yml@main with: job-name: tests-py-torchscript-fe @@ -99,6 +100,7 @@ jobs: - repository: pytorch/tensorrt package-name: torch_tensorrt pre-script: packaging/pre_build_script.sh + post-script: packaging/post_build_script.sh uses: pytorch/tensorrt/.github/workflows/linux-test.yml@main with: job-name: tests-py-dynamo-converters @@ -126,6 +128,7 @@ jobs: - repository: pytorch/tensorrt package-name: torch_tensorrt pre-script: packaging/pre_build_script.sh + post-script: packaging/post_build_script.sh uses: pytorch/tensorrt/.github/workflows/linux-test.yml@main with: job-name: tests-py-dynamo-fe @@ -154,6 +157,7 @@ jobs: - repository: pytorch/tensorrt package-name: torch_tensorrt pre-script: packaging/pre_build_script.sh + post-script: packaging/post_build_script.sh uses: pytorch/tensorrt/.github/workflows/linux-test.yml@main with: job-name: tests-py-dynamo-serde @@ -181,6 +185,7 @@ jobs: - repository: pytorch/tensorrt package-name: torch_tensorrt pre-script: packaging/pre_build_script.sh + post-script: packaging/post_build_script.sh uses: pytorch/tensorrt/.github/workflows/linux-test.yml@main with: job-name: tests-py-torch-compile-be @@ -209,6 +214,7 @@ jobs: - repository: pytorch/tensorrt package-name: torch_tensorrt pre-script: packaging/pre_build_script.sh + post-script: packaging/post_build_script.sh uses: pytorch/tensorrt/.github/workflows/linux-test.yml@main with: job-name: tests-py-dynamo-core diff --git a/packaging/post_build_script.sh b/packaging/post_build_script.sh index e69de29bb2..c42774070a 100644 --- a/packaging/post_build_script.sh +++ b/packaging/post_build_script.sh @@ -0,0 +1 @@ +python -m pip install /opt/torch-tensorrt-builds/TensorRT-10.0.0.6/python/tensorrt-10.0.0b6-cp38-none-linux_x86_64.whl \ No newline at end of file From 89f04dbbaf50075e7f4c0e5f4fe86f04ada43f2c Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 8 Apr 2024 22:07:54 -0700 Subject: [PATCH 045/122] chore: remove trt dep --- pyproject.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dd4cada7f2..acad8561c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,6 @@ requires = [ "cffi>=1.15.1", "typing-extensions>=4.7.0", "future>=0.18.3", - "tensorrt==10.0.0b6", "torch==2.3.0", "pybind11==2.6.2", "numpy", @@ -42,7 +41,6 @@ requires-python = ">=3.8" keywords = ["pytorch", "torch", "tensorrt", "trt", "ai", "artificial intelligence", "ml", "machine learning", "dl", "deep learning", "compiler", "dynamo", "torchscript", "inference"] dependencies = [ "torch==2.3.0", - "tensorrt==10.0.0b6", "packaging>=23", "numpy", "typing-extensions>=4.7.0", From 7620acca4d2d73e01d3edf9af9bf51dc29075d5b Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 8 Apr 2024 22:20:14 -0700 Subject: [PATCH 046/122] chore: updates --- packaging/pre_build_script.sh | 2 +- py/requirements.txt | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packaging/pre_build_script.sh b/packaging/pre_build_script.sh index 5905e44d2e..3243afd69f 100755 --- a/packaging/pre_build_script.sh +++ b/packaging/pre_build_script.sh @@ -10,7 +10,7 @@ export LD_LIBRARY_PATH=/opt/torch-tensorrt-builds/TensorRT-10.0.0.6/lib:$LD_LIBR wget https://github.com/bazelbuild/bazelisk/releases/download/v1.17.0/bazelisk-linux-amd64 \ && mv bazelisk-linux-amd64 /usr/bin/bazel \ && chmod +x /usr/bin/bazel - +python -m pip install -r py/requirements.txt export TORCH_BUILD_NUMBER=$(python -c "import torch, urllib.parse as ul; print(ul.quote_plus(torch.__version__))") cat toolchains/ci_workspaces/WORKSPACE.x86_64.release.rhel.tmpl | envsubst > WORKSPACE diff --git a/py/requirements.txt b/py/requirements.txt index 19c39c8647..360fdbb01d 100644 --- a/py/requirements.txt +++ b/py/requirements.txt @@ -5,5 +5,10 @@ pybind11==2.6.2 torch==2.3.0 torchvision==0.18.0 --extra-index-url https://pypi.ngc.nvidia.com -tensorrt>=10.0 pyyaml +# Extras +ninja>=1.11.0 +cffi>=1.15.1 +pybind11==2.6.2 +wheel>=0.40.0 +future>=0.18.3 From 62332fb4236442ca8e08f8323cdad10c3bfec9c0 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 9 Apr 2024 00:17:52 -0700 Subject: [PATCH 047/122] chore: set ld_library path in post script --- packaging/post_build_script.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/packaging/post_build_script.sh b/packaging/post_build_script.sh index c42774070a..1ee8f77467 100644 --- a/packaging/post_build_script.sh +++ b/packaging/post_build_script.sh @@ -1 +1,2 @@ +export LD_LIBRARY_PATH=/opt/torch-tensorrt-builds/TensorRT-10.0.0.6/lib:$LD_LIBRARY_PATH python -m pip install /opt/torch-tensorrt-builds/TensorRT-10.0.0.6/python/tensorrt-10.0.0b6-cp38-none-linux_x86_64.whl \ No newline at end of file From 96a8bf61e4027634d8bd85f3e6a4411816912c81 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 9 Apr 2024 13:20:29 -0700 Subject: [PATCH 048/122] chore: updates --- packaging/pre_build_script.sh | 2 +- py/torch_tensorrt/__init__.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packaging/pre_build_script.sh b/packaging/pre_build_script.sh index 3243afd69f..c518992ab6 100755 --- a/packaging/pre_build_script.sh +++ b/packaging/pre_build_script.sh @@ -3,7 +3,7 @@ # Install dependencies python3 -m pip install pyyaml yum install -y ninja-build gettext -TRT_VERSION=$(python3 -c "import versions; versions.tensorrt_version()") +TRT_VERSION=10.0.0.6 #$(python3 -c "import versions; versions.tensorrt_version()") wget -P /opt/torch-tensorrt-builds/ https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.0.0/TensorRT-10.0.0.6.Linux.x86_64-gnu.cuda-12.4.tar.gz tar -xvzf /opt/torch-tensorrt-builds/TensorRT-10.0.0.6.Linux.x86_64-gnu.cuda-12.4.tar.gz -C /opt/torch-tensorrt-builds/ export LD_LIBRARY_PATH=/opt/torch-tensorrt-builds/TensorRT-10.0.0.6/lib:$LD_LIBRARY_PATH diff --git a/py/torch_tensorrt/__init__.py b/py/torch_tensorrt/__init__.py index b9d2af39c5..f97de95d52 100644 --- a/py/torch_tensorrt/__init__.py +++ b/py/torch_tensorrt/__init__.py @@ -60,7 +60,7 @@ def _find_lib(name: str, paths: List[str]) -> str: elif sys.platform.startswith("linux"): LINUX_PATHS = ["/usr/local/cuda-12.1/lib64", "/usr/lib", "/usr/lib64"] - + print("==== BOOL CHECK", "LD_LIBRARY_PATH" in os.environ) if "LD_LIBRARY_PATH" in os.environ: LINUX_PATHS += os.environ["LD_LIBRARY_PATH"].split(os.path.pathsep) @@ -92,9 +92,8 @@ def _find_lib(name: str, paths: List[str]) -> str: from torch_tensorrt.runtime import * # noqa: F403 if version.parse(sanitized_torch_version()) >= version.parse("2.1.dev"): - from torch_tensorrt.dynamo import backend # noqa: F401 - from torch_tensorrt import dynamo # noqa: F401 + from torch_tensorrt.dynamo import backend # noqa: F401 def _register_with_torch() -> None: From 041f6a38e834bb94004c0a4910f08794a3024992 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 9 Apr 2024 13:48:58 -0700 Subject: [PATCH 049/122] chore: updates --- packaging/post_build_script.sh | 1 + py/torch_tensorrt/__init__.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packaging/post_build_script.sh b/packaging/post_build_script.sh index 1ee8f77467..0d5d045cd0 100644 --- a/packaging/post_build_script.sh +++ b/packaging/post_build_script.sh @@ -1,2 +1,3 @@ export LD_LIBRARY_PATH=/opt/torch-tensorrt-builds/TensorRT-10.0.0.6/lib:$LD_LIBRARY_PATH +echo $LD_LIBRARY_PATH python -m pip install /opt/torch-tensorrt-builds/TensorRT-10.0.0.6/python/tensorrt-10.0.0b6-cp38-none-linux_x86_64.whl \ No newline at end of file diff --git a/py/torch_tensorrt/__init__.py b/py/torch_tensorrt/__init__.py index f97de95d52..01f310637b 100644 --- a/py/torch_tensorrt/__init__.py +++ b/py/torch_tensorrt/__init__.py @@ -60,7 +60,12 @@ def _find_lib(name: str, paths: List[str]) -> str: elif sys.platform.startswith("linux"): LINUX_PATHS = ["/usr/local/cuda-12.1/lib64", "/usr/lib", "/usr/lib64"] - print("==== BOOL CHECK", "LD_LIBRARY_PATH" in os.environ) + print( + "==== BOOL CHECK", + "LD_LIBRARY_PATH" in os.environ, + " path: ", + os.environ["LD_LIBRARY_PATH"], + ) if "LD_LIBRARY_PATH" in os.environ: LINUX_PATHS += os.environ["LD_LIBRARY_PATH"].split(os.path.pathsep) From 83e9a0bc4f7e4281da00ea56df03f965883e285a Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 9 Apr 2024 15:31:31 -0700 Subject: [PATCH 050/122] chore: disable smoke test --- .github/workflows/build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index febc475c09..b05cb1e276 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -37,7 +37,7 @@ jobs: pre-script: packaging/pre_build_script.sh env-var-script: packaging/env_vars.txt post-script: packaging/post_build_script.sh - smoke-test-script: "" + smoke-test-script: packaging/smoke_test_script.sh package-name: torch_tensorrt name: Build torch-tensorrt whl package uses: pytorch/test-infra/.github/workflows/build_wheels_linux.yml@main From e8529b057e7658654114e31a2e5fecca7ec0faf9 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 9 Apr 2024 15:51:56 -0700 Subject: [PATCH 051/122] chore: updates --- .github/workflows/build-test.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index b05cb1e276..330006f76e 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -15,7 +15,7 @@ on: jobs: generate-matrix: - uses: pytorch/test-infra/.github/workflows/generate_binary_build_matrix.yml@main + uses: pytorch/test-infra/.github/workflows/generate_binary_build_matrix.yml@release/2.3 with: package-type: wheel os: linux @@ -40,7 +40,7 @@ jobs: smoke-test-script: packaging/smoke_test_script.sh package-name: torch_tensorrt name: Build torch-tensorrt whl package - uses: pytorch/test-infra/.github/workflows/build_wheels_linux.yml@main + uses: pytorch/test-infra/.github/workflows/build_wheels_linux.yml@release/2.3 with: repository: ${{ matrix.repository }} ref: "" @@ -65,7 +65,7 @@ jobs: package-name: torch_tensorrt pre-script: packaging/pre_build_script.sh post-script: packaging/post_build_script.sh - uses: pytorch/tensorrt/.github/workflows/linux-test.yml@main + uses: pytorch/tensorrt/.github/workflows/linux-test.yml@release/2.3 with: job-name: tests-py-torchscript-fe repository: "pytorch/tensorrt" @@ -101,7 +101,7 @@ jobs: package-name: torch_tensorrt pre-script: packaging/pre_build_script.sh post-script: packaging/post_build_script.sh - uses: pytorch/tensorrt/.github/workflows/linux-test.yml@main + uses: pytorch/tensorrt/.github/workflows/linux-test.yml@release/2.3 with: job-name: tests-py-dynamo-converters repository: "pytorch/tensorrt" @@ -129,7 +129,7 @@ jobs: package-name: torch_tensorrt pre-script: packaging/pre_build_script.sh post-script: packaging/post_build_script.sh - uses: pytorch/tensorrt/.github/workflows/linux-test.yml@main + uses: pytorch/tensorrt/.github/workflows/linux-test.yml@release/2.3 with: job-name: tests-py-dynamo-fe repository: "pytorch/tensorrt" @@ -158,7 +158,7 @@ jobs: package-name: torch_tensorrt pre-script: packaging/pre_build_script.sh post-script: packaging/post_build_script.sh - uses: pytorch/tensorrt/.github/workflows/linux-test.yml@main + uses: pytorch/tensorrt/.github/workflows/linux-test.yml@release/2.3 with: job-name: tests-py-dynamo-serde repository: "pytorch/tensorrt" @@ -186,7 +186,7 @@ jobs: package-name: torch_tensorrt pre-script: packaging/pre_build_script.sh post-script: packaging/post_build_script.sh - uses: pytorch/tensorrt/.github/workflows/linux-test.yml@main + uses: pytorch/tensorrt/.github/workflows/linux-test.yml@release/2.3 with: job-name: tests-py-torch-compile-be repository: "pytorch/tensorrt" @@ -215,7 +215,7 @@ jobs: package-name: torch_tensorrt pre-script: packaging/pre_build_script.sh post-script: packaging/post_build_script.sh - uses: pytorch/tensorrt/.github/workflows/linux-test.yml@main + uses: pytorch/tensorrt/.github/workflows/linux-test.yml@release/2.3 with: job-name: tests-py-dynamo-core repository: "pytorch/tensorrt" From 1357112a12995d25f9c6865544e8a71434d8a945 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 9 Apr 2024 16:41:16 -0700 Subject: [PATCH 052/122] chore: updates --- packaging/smoke_test_script.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 packaging/smoke_test_script.sh diff --git a/packaging/smoke_test_script.sh b/packaging/smoke_test_script.sh new file mode 100644 index 0000000000..e69de29bb2 From 608a6d274abea87d0939bb0917ae7e42c9efb605 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Wed, 10 Apr 2024 08:20:39 -0700 Subject: [PATCH 053/122] chore: updates --- .github/scripts/install-torch-tensorrt.sh | 12 +++++++++++- .github/workflows/build-test.yml | 3 ++- packaging/post_build_script.sh | 3 --- packaging/smoke_test_script.sh | 6 ++++++ py/requirements.txt | 8 +------- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/.github/scripts/install-torch-tensorrt.sh b/.github/scripts/install-torch-tensorrt.sh index 48c6c90cbf..10d5023557 100644 --- a/.github/scripts/install-torch-tensorrt.sh +++ b/.github/scripts/install-torch-tensorrt.sh @@ -5,6 +5,16 @@ source ${BUILD_ENV_FILE} ${CONDA_RUN} ${PIP_INSTALL_TORCH} torchvision ${CONDA_RUN} python -m pip install pyyaml mpmath==1.3.0 export TRT_VERSION=$(${CONDA_RUN} python -c "import versions; versions.tensorrt_version()") -${CONDA_RUN} python -m pip install /opt/torch-tensorrt-builds/torch_tensorrt*+${CU_VERSION}*.whl tensorrt~=${TRT_VERSION} tensorrt-bindings~=${TRT_VERSION} --extra-index-url=https://pypi.ngc.nvidia.com + +# Print PYTHON_VERSION +printf "PYTHON_VERSION is equal to %s" ${PYTHON_VERSION//./} + +# Install TensorRT manually +wget -P /opt/torch-tensorrt-builds/ https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.0.0/TensorRT-10.0.0.6.Linux.x86_64-gnu.cuda-12.4.tar.gz +tar -xvzf /opt/torch-tensorrt-builds/TensorRT-10.0.0.6.Linux.x86_64-gnu.cuda-12.4.tar.gz -C /opt/torch-tensorrt-builds/ +python -m pip install /opt/torch-tensorrt-builds/TensorRT-10.0.0.6/python/tensorrt-10.0.0b6-cp${PYTHON_VERSION//./}-none-linux_x86_64.whl + +# Install Torch-TensorRT +${CONDA_RUN} python -m pip install /opt/torch-tensorrt-builds/torch_tensorrt*+${CU_VERSION}*.whl echo -e "Running test script"; diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 330006f76e..89b41df3a3 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -79,7 +79,8 @@ jobs: export LD_LIBRARY_PATH=/usr/lib64:$LD_LIBRARY_PATH pushd . cd tests/modules - ${CONDA_RUN} python -m pip install --pre -r requirements.txt --use-deprecated=legacy-resolver + # Don't use requirements.txt here as it contains tensorrt and torch which should have been installed by now. + ${CONDA_RUN} python -m pip install numpy packaging pyyaml pybind11==2.6.2 ${CONDA_RUN} python hub.py popd pushd . diff --git a/packaging/post_build_script.sh b/packaging/post_build_script.sh index 0d5d045cd0..e69de29bb2 100644 --- a/packaging/post_build_script.sh +++ b/packaging/post_build_script.sh @@ -1,3 +0,0 @@ -export LD_LIBRARY_PATH=/opt/torch-tensorrt-builds/TensorRT-10.0.0.6/lib:$LD_LIBRARY_PATH -echo $LD_LIBRARY_PATH -python -m pip install /opt/torch-tensorrt-builds/TensorRT-10.0.0.6/python/tensorrt-10.0.0b6-cp38-none-linux_x86_64.whl \ No newline at end of file diff --git a/packaging/smoke_test_script.sh b/packaging/smoke_test_script.sh index e69de29bb2..d3bed3249e 100644 --- a/packaging/smoke_test_script.sh +++ b/packaging/smoke_test_script.sh @@ -0,0 +1,6 @@ +# Smoke test is intentionally disabled. +# The issue was smoke test installs the built torch_tensorrt wheel file and checks `import torch_tensorrt; print(torch_tensorrt.__version__)` +# Since tensorrt cannot be pip installable in CI, the smoke test will fail. +# One way we tried to handle it is manually install tensorrt wheel while by extracting from the tarball. +# However, the TensorRT-10.0.0.6/lib path doesn't seem to show up in LD_LIBRARY_PATH even if we explicitly set it. +# TODO: Implement a custom smoke_test script to verify torch_tensorrt installation. \ No newline at end of file diff --git a/py/requirements.txt b/py/requirements.txt index 360fdbb01d..e3356a8c59 100644 --- a/py/requirements.txt +++ b/py/requirements.txt @@ -5,10 +5,4 @@ pybind11==2.6.2 torch==2.3.0 torchvision==0.18.0 --extra-index-url https://pypi.ngc.nvidia.com -pyyaml -# Extras -ninja>=1.11.0 -cffi>=1.15.1 -pybind11==2.6.2 -wheel>=0.40.0 -future>=0.18.3 +pyyaml \ No newline at end of file From 1b34b3246da47b4a621b8d818b8e57d86a9679d0 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Wed, 10 Apr 2024 08:56:19 -0700 Subject: [PATCH 054/122] chore: updates --- .github/workflows/build-test.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 89b41df3a3..278a4aecf3 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -77,6 +77,7 @@ jobs: script: | export USE_HOST_DEPS=1 export LD_LIBRARY_PATH=/usr/lib64:$LD_LIBRARY_PATH + export LD_LIBRARY_PATH=/opt/torch-tensorrt-builds/TensorRT-10.0.0.6/lib:$LD_LIBRARY_PATH pushd . cd tests/modules # Don't use requirements.txt here as it contains tensorrt and torch which should have been installed by now. @@ -113,6 +114,7 @@ jobs: pre-script: ${{ matrix.pre-script }} script: | export USE_HOST_DEPS=1 + export LD_LIBRARY_PATH=/opt/torch-tensorrt-builds/TensorRT-10.0.0.6/lib:$LD_LIBRARY_PATH pushd . cd tests/py/dynamo ${CONDA_RUN} python -m pip install --pre pytest-xdist timm transformers parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver @@ -141,6 +143,7 @@ jobs: pre-script: ${{ matrix.pre-script }} script: | export USE_HOST_DEPS=1 + export LD_LIBRARY_PATH=/opt/torch-tensorrt-builds/TensorRT-10.0.0.6/lib:$LD_LIBRARY_PATH pushd . cd tests/py/dynamo ${CONDA_RUN} python -m pip install --pre pytest timm transformers parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver @@ -170,6 +173,7 @@ jobs: pre-script: ${{ matrix.pre-script }} script: | export USE_HOST_DEPS=1 + export LD_LIBRARY_PATH=/opt/torch-tensorrt-builds/TensorRT-10.0.0.6/lib:$LD_LIBRARY_PATH pushd . cd tests/py/dynamo ${CONDA_RUN} python -m pip install --pre pytest timm transformers parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver @@ -198,6 +202,7 @@ jobs: pre-script: ${{ matrix.pre-script }} script: | export USE_HOST_DEPS=1 + export LD_LIBRARY_PATH=/opt/torch-tensorrt-builds/TensorRT-10.0.0.6/lib:$LD_LIBRARY_PATH pushd . cd tests/py/dynamo ${CONDA_RUN} python -m pip install --pre pytest-xdist timm transformers parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver @@ -227,6 +232,7 @@ jobs: pre-script: ${{ matrix.pre-script }} script: | export USE_HOST_DEPS=1 + export LD_LIBRARY_PATH=/opt/torch-tensorrt-builds/TensorRT-10.0.0.6/lib:$LD_LIBRARY_PATH pushd . cd tests/py/dynamo ${CONDA_RUN} python -m pip install --pre pytest-xdist timm transformers parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver From 89cb55a6af0aeacf4063ea5510c3049a5763e401 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Wed, 10 Apr 2024 11:45:13 -0700 Subject: [PATCH 055/122] chore: updates --- .github/workflows/build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 278a4aecf3..c32d42c78e 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -81,7 +81,7 @@ jobs: pushd . cd tests/modules # Don't use requirements.txt here as it contains tensorrt and torch which should have been installed by now. - ${CONDA_RUN} python -m pip install numpy packaging pyyaml pybind11==2.6.2 + ${CONDA_RUN} python -m pip install numpy packaging pyyaml transformers pybind11==2.6.2 ${CONDA_RUN} python hub.py popd pushd . From 4323e36461b4caeb90c9eca47e7f6887e92bed15 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Wed, 10 Apr 2024 12:41:49 -0700 Subject: [PATCH 056/122] chore: updates --- .github/workflows/build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index c32d42c78e..1b1fa72f2f 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -81,7 +81,7 @@ jobs: pushd . cd tests/modules # Don't use requirements.txt here as it contains tensorrt and torch which should have been installed by now. - ${CONDA_RUN} python -m pip install numpy packaging pyyaml transformers pybind11==2.6.2 + ${CONDA_RUN} python -m pip install numpy packaging pyyaml transformers timm pybind11==2.6.2 ${CONDA_RUN} python hub.py popd pushd . From 60b3e51bcc4e6fc93bc84909fe8cfb27617d54af Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Wed, 10 Apr 2024 15:07:23 -0700 Subject: [PATCH 057/122] chore: update hw_compat --- tests/py/dynamo/runtime/gen_hw_compat.py | 33 ++++++++++++++++++++++ tests/py/dynamo/runtime/test_hw_compat.py | 6 ++-- tests/py/ts/models/hw_compat.ts | Bin 386982 -> 110354 bytes 3 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 tests/py/dynamo/runtime/gen_hw_compat.py diff --git a/tests/py/dynamo/runtime/gen_hw_compat.py b/tests/py/dynamo/runtime/gen_hw_compat.py new file mode 100644 index 0000000000..e279015aa2 --- /dev/null +++ b/tests/py/dynamo/runtime/gen_hw_compat.py @@ -0,0 +1,33 @@ +# This script is used to generate hw_compat.ts file that's used in test_hw_compat.py +# Generate the model on a different hardware compared to the one you're testing on to +# verify HW compatibility feature. + +import torch +import torch_tensorrt + + +class MyModule(torch.nn.Module): + def __init__(self): + super().__init__() + self.conv = torch.nn.Conv2d(3, 16, 3, stride=1, bias=True) + self.relu = torch.nn.ReLU() + + def forward(self, x): + out = self.conv(x) + out = self.relu(out) + return out + + +model = MyModule().eval().cuda() +inputs = torch.randn((1, 3, 224, 224)).to("cuda") + +trt_gm = torch_tensorrt.compile( + model, + ir="dynamo", + inputs=inputs, + min_block_size=1, + hardware_compatible=True, + version_compatible=True, +) +trt_script_model = torch.jit.trace(trt_gm, inputs) +torch.jit.save(trt_script_model, "hw_compat.ts") diff --git a/tests/py/dynamo/runtime/test_hw_compat.py b/tests/py/dynamo/runtime/test_hw_compat.py index 4218cc7de0..aa238c7a4a 100644 --- a/tests/py/dynamo/runtime/test_hw_compat.py +++ b/tests/py/dynamo/runtime/test_hw_compat.py @@ -59,16 +59,14 @@ def forward(self, x): "Detected incorrect ABI version, please update this test case", ) def test_hw_compat_3080_build(self): - inputs = [torch.randn(5, 7).cuda()] + inputs = [torch.randn(1, 3, 224, 224).cuda()] cwd = os.getcwd() os.chdir(os.path.dirname(os.path.realpath(__file__))) model = torch.jit.load("../../ts/models/hw_compat.ts").cuda() out = model(*inputs) self.assertTrue( - isinstance(out, tuple) - and len(out) == 1 - and isinstance(out[0], torch.Tensor), + len(out) == 1 and isinstance(out, torch.Tensor), "Invalid output detected", ) os.chdir(cwd) diff --git a/tests/py/ts/models/hw_compat.ts b/tests/py/ts/models/hw_compat.ts index ab43e5e040ef3850c8e0b879a973dc759565830e..27726233f28119dc4c1b52793e03efc7da7fc3da 100644 GIT binary patch literal 110354 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�bkIVlk=Vn0ejK2GoxHx=dq8V-3rKe5rdnc~Ns! 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�h^Gt0EN&9mC;l63bF`3O_!F?>^fiumX{ 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

oIuGq0lfl!q<(lEn=@gd~4uOZ0Wgam{jB{6w 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 XkJtSs46Mc2I(t_qu<+#mx_kd0(QA

Date: Fri, 12 Apr 2024 14:45:50 -0700 Subject: [PATCH 058/122] chore: updates --- .../conversion/impl/normalization/ops.py | 14 +++++- .../dynamo/conversion/test_layer_norm_aten.py | 49 ------------------- 2 files changed, 13 insertions(+), 50 deletions(-) diff --git a/py/torch_tensorrt/dynamo/conversion/impl/normalization/ops.py b/py/torch_tensorrt/dynamo/conversion/impl/normalization/ops.py index f45d067349..bbe566d0b7 100644 --- a/py/torch_tensorrt/dynamo/conversion/impl/normalization/ops.py +++ b/py/torch_tensorrt/dynamo/conversion/impl/normalization/ops.py @@ -452,6 +452,7 @@ def pdist( p: float = 2, ) -> Union[TRTTensor, Sequence[TRTTensor]]: shape = input.shape + # Extend input from shape [N, D] to [N, 1, D] extend_input = impl.shuffle.reshape( ctx, target, @@ -460,7 +461,18 @@ def pdist( input, shape=shape[0:1] + (1,) + shape[1:], ) - x = impl.elementwise.sub(ctx, target, source_ir, f"{name}_sub", extend_input, input) + # Expand the input from [N, 1, D] to [N, N, D] + x = impl.slice.expand( + ctx, + target, + source_ir, + f"{name}_sub", + extend_input, + (shape[0], shape[0]) + shape[1:], + ) + # Subtract the expanded input from original input. Result shape = [N, N, D] + # This matrix has the distance of each sample to every other sample and hence the shape is [N, N, D] + x = impl.elementwise.sub(ctx, target, source_ir, f"{name}_sub", x, input) if p == 0: # norm = torch.sum(x!=0, dim=2) diff --git a/tests/py/dynamo/conversion/test_layer_norm_aten.py b/tests/py/dynamo/conversion/test_layer_norm_aten.py index 8013768214..7f43234211 100644 --- a/tests/py/dynamo/conversion/test_layer_norm_aten.py +++ b/tests/py/dynamo/conversion/test_layer_norm_aten.py @@ -24,31 +24,6 @@ def forward(self, x): inputs, ) - def test_layernorm_with_dynamic_shape(self): - class LayerNorm(torch.nn.Module): - def forward(self, x): - return torch.ops.aten.layer_norm.default( - x, - torch.tensor([3, 224, 224]), - torch.ones((3, 224, 224)), - torch.zeros((3, 224, 224)), - 1e-05, - True, - ) - - input_specs = [ - Input( - shape=(-1, 3, 224, 224), - dtype=torch.float32, - shape_ranges=[((1, 3, 224, 224), (1, 3, 224, 224), (2, 3, 224, 224))], - ), - ] - - self.run_test_with_dynamic_shape( - LayerNorm(), - input_specs, - ) - class TestNativeLayerNormConverter(DispatchTestCase): def test_layer_norm(self): @@ -68,30 +43,6 @@ def forward(self, x): inputs, ) - def test_layernorm_with_dynamic_shape(self): - class LayerNorm(torch.nn.Module): - def forward(self, x): - return torch.ops.aten.native_layer_norm.default( - x, - torch.tensor([3, 224, 224]), - torch.ones((3, 224, 224)), - torch.zeros((3, 224, 224)), - 1e-05, - )[0] - - input_specs = [ - Input( - shape=(-1, 3, 224, 224), - dtype=torch.float32, - shape_ranges=[((1, 3, 224, 224), (1, 3, 224, 224), (2, 3, 224, 224))], - ), - ] - - self.run_test_with_dynamic_shape( - LayerNorm(), - input_specs, - ) - if __name__ == "__main__": run_tests() From d16585f0be1c5085739d7c09b29acb81d359e921 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Fri, 12 Apr 2024 16:26:37 -0700 Subject: [PATCH 059/122] chore: update streams --- core/runtime/execute_engine.cpp | 3 +-- .../dynamo/runtime/_PythonTorchTensorRTModule.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/core/runtime/execute_engine.cpp b/core/runtime/execute_engine.cpp index 5ff163fbfb..165328f16c 100644 --- a/core/runtime/execute_engine.cpp +++ b/core/runtime/execute_engine.cpp @@ -178,8 +178,7 @@ std::vector execute_engine(std::vector inputs, c10::intr enqueue_profiler_guard = std::make_unique(compiled_engine->enqueue_profile_path); } - c10::cuda::CUDAStream stream = c10::cuda::getCurrentCUDAStream(inputs[0].device().index()); - + c10::cuda::CUDAStream stream = c10::cuda::getStreamFromPool(/*isHighPriority=*/true, inputs[0].device().index()); // nvinfer1::IExecutionContext::enqueue is not thread safe and we need a mutex for it. std::unique_lock lock(compiled_engine->mu); compiled_engine->exec_ctx->enqueueV3(stream); diff --git a/py/torch_tensorrt/dynamo/runtime/_PythonTorchTensorRTModule.py b/py/torch_tensorrt/dynamo/runtime/_PythonTorchTensorRTModule.py index 82f5817b01..a296deaba9 100644 --- a/py/torch_tensorrt/dynamo/runtime/_PythonTorchTensorRTModule.py +++ b/py/torch_tensorrt/dynamo/runtime/_PythonTorchTensorRTModule.py @@ -59,7 +59,7 @@ def _initialize(self) -> None: runtime = trt.Runtime(logger) self.engine = runtime.deserialize_cuda_engine(self.engine) self.context = self.engine.create_execution_context() - + self.stream = torch.cuda.Stream(torch.cuda.current_device()) # Indices of inputs/outputs in the trt engine bindings, in the order # as they are in the original PyTorch model. @@ -286,7 +286,7 @@ def forward(self, *inputs: torch.Tensor) -> torch.Tensor | Tuple[torch.Tensor, . if self.profiling_enabled else nullcontext() ): - self.context.execute_async_v3(torch.cuda.current_stream().cuda_stream) + self.context.execute_async_v3(self.stream.cuda_stream) if len(outputs) == 1: return outputs[0] From 3d149ef78a4e39c9a249b66790721d0e02a92e7d Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Fri, 12 Apr 2024 21:30:42 -0700 Subject: [PATCH 060/122] chore: updates --- py/torch_tensorrt/dynamo/conversion/impl/shape.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/py/torch_tensorrt/dynamo/conversion/impl/shape.py b/py/torch_tensorrt/dynamo/conversion/impl/shape.py index 0727eecef7..e19f44ab3a 100644 --- a/py/torch_tensorrt/dynamo/conversion/impl/shape.py +++ b/py/torch_tensorrt/dynamo/conversion/impl/shape.py @@ -9,6 +9,7 @@ from torch_tensorrt.dynamo._SourceIR import SourceIR from torch_tensorrt.dynamo.conversion._ConversionContext import ConversionContext from torch_tensorrt.dynamo.conversion.converter_utils import ( + cast_trt_tensor, get_positive_dim, get_trt_tensor, ) @@ -38,6 +39,12 @@ def shape( """ shape_layer = ctx.net.add_shape(input_val) input_shape = shape_layer.get_output(0) + input_shape = cast_trt_tensor( + ctx, + input_shape, + trt.int32, + name + "_shape_casted", + ) set_layer_name(shape_layer, target, name + "_shape", source_ir) n_dims = len(input_val.shape) @@ -82,6 +89,12 @@ def get_shape_with_dynamic_shape( """ # Ger real shape info for input_val input_shape = ctx.net.add_shape(input_val).get_output(0) + input_shape = cast_trt_tensor( + ctx, + input_shape, + trt.int32, + name + "_int32_casted", + ) # input_shape.dtype is int64 in TRT 10.0 input_np_dtype = unified_dtype_converter(input_shape.dtype, Frameworks.NUMPY) scale_layer = ctx.net.add_constant( From 3addcae373f550603c904dd110c4d906dbf4bfb9 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Sat, 13 Apr 2024 01:07:35 -0700 Subject: [PATCH 061/122] chore: updates --- .github/scripts/install-torch-tensorrt.sh | 7 ++----- core/runtime/execute_engine.cpp | 2 +- packaging/pre_build_script.sh | 6 +++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/scripts/install-torch-tensorrt.sh b/.github/scripts/install-torch-tensorrt.sh index 10d5023557..b2b19b139d 100644 --- a/.github/scripts/install-torch-tensorrt.sh +++ b/.github/scripts/install-torch-tensorrt.sh @@ -6,12 +6,9 @@ ${CONDA_RUN} ${PIP_INSTALL_TORCH} torchvision ${CONDA_RUN} python -m pip install pyyaml mpmath==1.3.0 export TRT_VERSION=$(${CONDA_RUN} python -c "import versions; versions.tensorrt_version()") -# Print PYTHON_VERSION -printf "PYTHON_VERSION is equal to %s" ${PYTHON_VERSION//./} - # Install TensorRT manually -wget -P /opt/torch-tensorrt-builds/ https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.0.0/TensorRT-10.0.0.6.Linux.x86_64-gnu.cuda-12.4.tar.gz -tar -xvzf /opt/torch-tensorrt-builds/TensorRT-10.0.0.6.Linux.x86_64-gnu.cuda-12.4.tar.gz -C /opt/torch-tensorrt-builds/ +wget -q -P /opt/torch-tensorrt-builds/ https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.0.0/TensorRT-10.0.0.6.Linux.x86_64-gnu.cuda-12.4.tar.gz +tar -xzf /opt/torch-tensorrt-builds/TensorRT-10.0.0.6.Linux.x86_64-gnu.cuda-12.4.tar.gz -C /opt/torch-tensorrt-builds/ python -m pip install /opt/torch-tensorrt-builds/TensorRT-10.0.0.6/python/tensorrt-10.0.0b6-cp${PYTHON_VERSION//./}-none-linux_x86_64.whl # Install Torch-TensorRT diff --git a/core/runtime/execute_engine.cpp b/core/runtime/execute_engine.cpp index 165328f16c..a1ee30e994 100644 --- a/core/runtime/execute_engine.cpp +++ b/core/runtime/execute_engine.cpp @@ -178,7 +178,7 @@ std::vector execute_engine(std::vector inputs, c10::intr enqueue_profiler_guard = std::make_unique(compiled_engine->enqueue_profile_path); } - c10::cuda::CUDAStream stream = c10::cuda::getStreamFromPool(/*isHighPriority=*/true, inputs[0].device().index()); + c10::cuda::CUDAStream stream = c10::cuda::getCurrentCUDAStream(inputs[0].device().index()); // nvinfer1::IExecutionContext::enqueue is not thread safe and we need a mutex for it. std::unique_lock lock(compiled_engine->mu); compiled_engine->exec_ctx->enqueueV3(stream); diff --git a/packaging/pre_build_script.sh b/packaging/pre_build_script.sh index c518992ab6..befb6e38d9 100755 --- a/packaging/pre_build_script.sh +++ b/packaging/pre_build_script.sh @@ -3,9 +3,9 @@ # Install dependencies python3 -m pip install pyyaml yum install -y ninja-build gettext -TRT_VERSION=10.0.0.6 #$(python3 -c "import versions; versions.tensorrt_version()") -wget -P /opt/torch-tensorrt-builds/ https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.0.0/TensorRT-10.0.0.6.Linux.x86_64-gnu.cuda-12.4.tar.gz -tar -xvzf /opt/torch-tensorrt-builds/TensorRT-10.0.0.6.Linux.x86_64-gnu.cuda-12.4.tar.gz -C /opt/torch-tensorrt-builds/ +TRT_VERSION=$(python3 -c "import versions; versions.tensorrt_version()") +wget -q -P /opt/torch-tensorrt-builds/ https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.0.0/TensorRT-10.0.0.6.Linux.x86_64-gnu.cuda-12.4.tar.gz +tar -xzf /opt/torch-tensorrt-builds/TensorRT-10.0.0.6.Linux.x86_64-gnu.cuda-12.4.tar.gz -C /opt/torch-tensorrt-builds/ export LD_LIBRARY_PATH=/opt/torch-tensorrt-builds/TensorRT-10.0.0.6/lib:$LD_LIBRARY_PATH wget https://github.com/bazelbuild/bazelisk/releases/download/v1.17.0/bazelisk-linux-amd64 \ && mv bazelisk-linux-amd64 /usr/bin/bazel \ From b0e92d87979bf10d5a7b7ac356868c8f006f7787 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 15 Apr 2024 12:49:39 -0700 Subject: [PATCH 062/122] chore: update hw_compat.ts --- tests/py/ts/models/hw_compat.ts | Bin 110354 -> 110418 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/py/ts/models/hw_compat.ts b/tests/py/ts/models/hw_compat.ts index 27726233f28119dc4c1b52793e03efc7da7fc3da..ed71663c0c77a406307a3e7c23d331c1b44b7243 100644 GIT binary patch delta 5077 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 From d285d27d6384fa8101fcebe74136400d42effca8 Mon Sep 17 00:00:00 2001 From: Evan Li Date: Mon, 15 Apr 2024 14:06:20 -0700 Subject: [PATCH 063/122] fix dynamic shape bugs for test_binary_ops_aten --- .../conversion/impl/elementwise/base.py | 71 +++++++++++-------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/py/torch_tensorrt/dynamo/conversion/impl/elementwise/base.py b/py/torch_tensorrt/dynamo/conversion/impl/elementwise/base.py index f93ec52504..d835110159 100644 --- a/py/torch_tensorrt/dynamo/conversion/impl/elementwise/base.py +++ b/py/torch_tensorrt/dynamo/conversion/impl/elementwise/base.py @@ -13,7 +13,11 @@ cast_trt_tensor, get_trt_tensor, ) -from torch_tensorrt.fx.converters.converter_utils import set_layer_name +from torch_tensorrt.fx.converters.converter_utils import ( + broadcast, + has_dynamic_shape, + set_layer_name, +) from torch_tensorrt.fx.types import TRTElementWiseOp, TRTTensor from torch_tensorrt.fx.utils import Frameworks, unified_dtype_converter @@ -148,38 +152,43 @@ def convert_binary_elementwise( ctx, rhs_val, trt_promoted_type, f"{name}_cast_rhs_val", target, source_ir ) - lhs_val_shape = lhs_val.shape - rhs_val_shape = rhs_val.shape - rank_diff = len(lhs_val_shape) - len(rhs_val_shape) - if rank_diff > 0: - rhs_val = impl.slice.expand( - ctx, target, source_ir, f"{name}_expand_rhs_val", rhs_val, lhs_val_shape - ) - elif rank_diff < 0: - lhs_val = impl.slice.expand( - ctx, target, source_ir, f"{name}_expand_lhs_val", lhs_val, rhs_val_shape + if has_dynamic_shape(lhs_val.shape) or has_dynamic_shape(rhs_val.shape): + lhs_val, rhs_val = broadcast( + ctx.net, lhs_val, rhs_val, f"{name}_lhs", f"{name}_rhs" ) else: - if tuple(lhs_val_shape) != tuple(rhs_val_shape): - sum_diff = sum(lhs_val_shape) - sum(rhs_val_shape) - if sum_diff > 0: - rhs_val = impl.slice.expand( - ctx, - target, - source_ir, - f"{name}_expand_rhs_val", - rhs_val, - lhs_val_shape, - ) - elif sum_diff < 0: - lhs_val = impl.slice.expand( - ctx, - target, - source_ir, - f"{name}_expand_lhs_val", - lhs_val, - rhs_val_shape, - ) + lhs_val_shape = lhs_val.shape + rhs_val_shape = rhs_val.shape + rank_diff = len(lhs_val_shape) - len(rhs_val_shape) + if rank_diff > 0: + rhs_val = impl.slice.expand( + ctx, target, source_ir, f"{name}_expand_rhs_val", rhs_val, lhs_val_shape + ) + elif rank_diff < 0: + lhs_val = impl.slice.expand( + ctx, target, source_ir, f"{name}_expand_lhs_val", lhs_val, rhs_val_shape + ) + else: + if tuple(lhs_val_shape) != tuple(rhs_val_shape): + sum_diff = sum(lhs_val_shape) - sum(rhs_val_shape) + if sum_diff > 0: + rhs_val = impl.slice.expand( + ctx, + target, + source_ir, + f"{name}_expand_rhs_val", + rhs_val, + lhs_val_shape, + ) + elif sum_diff < 0: + lhs_val = impl.slice.expand( + ctx, + target, + source_ir, + f"{name}_expand_lhs_val", + lhs_val, + rhs_val_shape, + ) layer = ctx.net.add_elementwise(lhs_val, rhs_val, op_type) set_layer_name(layer, target, name, source_ir) From d78a8466c961ae11e061ea4d253a0bcb4809cbcd Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 15 Apr 2024 18:40:05 -0700 Subject: [PATCH 064/122] chore: revert layer_norm test --- .../dynamo/conversion/test_layer_norm_aten.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/py/dynamo/conversion/test_layer_norm_aten.py b/tests/py/dynamo/conversion/test_layer_norm_aten.py index 7f43234211..8013768214 100644 --- a/tests/py/dynamo/conversion/test_layer_norm_aten.py +++ b/tests/py/dynamo/conversion/test_layer_norm_aten.py @@ -24,6 +24,31 @@ def forward(self, x): inputs, ) + def test_layernorm_with_dynamic_shape(self): + class LayerNorm(torch.nn.Module): + def forward(self, x): + return torch.ops.aten.layer_norm.default( + x, + torch.tensor([3, 224, 224]), + torch.ones((3, 224, 224)), + torch.zeros((3, 224, 224)), + 1e-05, + True, + ) + + input_specs = [ + Input( + shape=(-1, 3, 224, 224), + dtype=torch.float32, + shape_ranges=[((1, 3, 224, 224), (1, 3, 224, 224), (2, 3, 224, 224))], + ), + ] + + self.run_test_with_dynamic_shape( + LayerNorm(), + input_specs, + ) + class TestNativeLayerNormConverter(DispatchTestCase): def test_layer_norm(self): @@ -43,6 +68,30 @@ def forward(self, x): inputs, ) + def test_layernorm_with_dynamic_shape(self): + class LayerNorm(torch.nn.Module): + def forward(self, x): + return torch.ops.aten.native_layer_norm.default( + x, + torch.tensor([3, 224, 224]), + torch.ones((3, 224, 224)), + torch.zeros((3, 224, 224)), + 1e-05, + )[0] + + input_specs = [ + Input( + shape=(-1, 3, 224, 224), + dtype=torch.float32, + shape_ranges=[((1, 3, 224, 224), (1, 3, 224, 224), (2, 3, 224, 224))], + ), + ] + + self.run_test_with_dynamic_shape( + LayerNorm(), + input_specs, + ) + if __name__ == "__main__": run_tests() From 68aab70d633a9d35a08f32209fbdbbde4ad2000d Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Wed, 17 Apr 2024 16:18:45 -0700 Subject: [PATCH 065/122] chore: updates --- docker/Dockerfile | 65 ++++++++++++++++++++++------------------------- docker/README.md | 6 ++--- 2 files changed, 34 insertions(+), 37 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 60b213b110..8e9918d0ba 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -8,9 +8,6 @@ ENV BASE_IMG=nvidia/cuda:12.1.1-devel-ubuntu22.04 ARG TENSORRT_VERSION ENV TENSORRT_VERSION=${TENSORRT_VERSION} RUN test -n "$TENSORRT_VERSION" || (echo "No tensorrt version specified, please use --build-arg TENSORRT_VERSION=x.y to specify a version." && exit 1) -ARG CUDNN_VERSION -ENV CUDNN_VERSION=${CUDNN_VERSION} -RUN test -n "$CUDNN_VERSION" || (echo "No cudnn version specified, please use --build-arg CUDNN_VERSION=x.y to specify a version." && exit 1) ARG PYTHON_VERSION=3.10 ENV PYTHON_VERSION=${PYTHON_VERSION} @@ -36,18 +33,18 @@ RUN pyenv install -v ${PYTHON_VERSION} RUN pyenv global ${PYTHON_VERSION} # Install CUDNN + TensorRT + dependencies -RUN wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-ubuntu2204.pin -RUN mv cuda-ubuntu2204.pin /etc/apt/preferences.d/cuda-repository-pin-600 -RUN apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/7fa2af80.pub -RUN add-apt-repository "deb https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/ /" -RUN apt-get update -RUN apt-get install -y libcudnn8=${CUDNN_VERSION}* libcudnn8-dev=${CUDNN_VERSION}* +# RUN wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-ubuntu2204.pin +# RUN mv cuda-ubuntu2204.pin /etc/apt/preferences.d/cuda-repository-pin-600 +# RUN apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/7fa2af80.pub +# RUN add-apt-repository "deb https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/ /" +# RUN apt-get update +# RUN apt-get install -y libcudnn8=${CUDNN_VERSION}* libcudnn8-dev=${CUDNN_VERSION}* RUN apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/3bf863cc.pub RUN add-apt-repository "deb https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/ /" RUN apt-get update -RUN apt-get install -y libnvinfer8=${TENSORRT_VERSION}.* libnvinfer-plugin8=${TENSORRT_VERSION}.* libnvinfer-dev=${TENSORRT_VERSION}.* libnvinfer-plugin-dev=${TENSORRT_VERSION}.* libnvonnxparsers8=${TENSORRT_VERSION}.* libnvonnxparsers-dev=${TENSORRT_VERSION}.* libnvparsers8=${TENSORRT_VERSION}.* libnvparsers-dev=${TENSORRT_VERSION}.* +RUN apt-get install -y libnvinfer10=${TENSORRT_VERSION}.* libnvinfer-plugin10=${TENSORRT_VERSION}.* libnvinfer-dev=${TENSORRT_VERSION}.* libnvinfer-plugin-dev=${TENSORRT_VERSION}.* libnvonnxparsers10=${TENSORRT_VERSION}.* libnvonnxparsers-dev=${TENSORRT_VERSION}.* # Setup Bazel via Bazelisk RUN wget -q https://github.com/bazelbuild/bazelisk/releases/download/v1.17.0/bazelisk-linux-amd64 -O /usr/bin/bazel &&\ @@ -88,35 +85,35 @@ RUN CUDA_BASE_IMG_VERSION_INTERMEDIATE=`echo ${BASE_IMG#*:}` &&\ ENV CUDA_HOME=/usr/local/cuda -# This script builds both libtorchtrt bin/lib/include tarball and the Python wheel, in dist/ -RUN bash ./docker/dist-build.sh +# # This script builds both libtorchtrt bin/lib/include tarball and the Python wheel, in dist/ +# RUN bash ./docker/dist-build.sh -# Copy and install Torch-TRT into the main container -FROM base as torch-tensorrt +# # Copy and install Torch-TRT into the main container +# FROM base as torch-tensorrt -COPY . /opt/torch_tensorrt +# COPY . /opt/torch_tensorrt -# Symlink the path pyenv is using for python with the /opt directory for package sourcing -RUN mkdir -p "/opt/python3/" &&\ - ln -s "`pyenv which python | xargs dirname | xargs dirname`/lib/python$PYTHON_VERSION/site-packages" "/opt/python3/" +# # Symlink the path pyenv is using for python with the /opt directory for package sourcing +# RUN mkdir -p "/opt/python3/" &&\ +# ln -s "`pyenv which python | xargs dirname | xargs dirname`/lib/python$PYTHON_VERSION/site-packages" "/opt/python3/" -COPY --from=torch-tensorrt-builder /workspace/torch_tensorrt/src/dist/ . +# COPY --from=torch-tensorrt-builder /workspace/torch_tensorrt/src/dist/ . -RUN cp /opt/torch_tensorrt/docker/WORKSPACE.docker /opt/torch_tensorrt/WORKSPACE &&\ - pip install -r /opt/torch_tensorrt/py/requirements.txt &&\ - # Install all dependency wheel files and user-specified TensorRT - pip install *.whl &&\ - pip install tensorrt==${TENSORRT_VERSION}.* &&\ - # Add the Torch-TensorRT wheel file to the dist directory and delete all other .whl files - rm -fr /workspace/torch_tensorrt/dist/* &&\ - mkdir -p /opt/torch_tensorrt/dist/ && mv torch_tensorrt*.whl /opt/torch_tensorrt/dist/ &&\ - rm -fr *.whl &&\ - # Remove other cache files if present - pip cache purge && rm -rf /opt/torch_tensorrt/.mypy_cache +# RUN cp /opt/torch_tensorrt/docker/WORKSPACE.docker /opt/torch_tensorrt/WORKSPACE &&\ +# pip install -r /opt/torch_tensorrt/py/requirements.txt &&\ +# # Install all dependency wheel files and user-specified TensorRT +# pip install *.whl &&\ +# pip install tensorrt==${TENSORRT_VERSION}.* &&\ +# # Add the Torch-TensorRT wheel file to the dist directory and delete all other .whl files +# rm -fr /workspace/torch_tensorrt/dist/* &&\ +# mkdir -p /opt/torch_tensorrt/dist/ && mv torch_tensorrt*.whl /opt/torch_tensorrt/dist/ &&\ +# rm -fr *.whl &&\ +# # Remove other cache files if present +# pip cache purge && rm -rf /opt/torch_tensorrt/.mypy_cache -WORKDIR /opt/torch_tensorrt +# WORKDIR /opt/torch_tensorrt -ENV LD_LIBRARY_PATH /opt/python3/site-packages/torch/lib:/opt/python3/site-packages/torch_tensorrt/lib:/usr/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH} -ENV PATH /opt/python3/site-packages/torch_tensorrt/bin:${PATH} +# ENV LD_LIBRARY_PATH /opt/python3/site-packages/torch/lib:/opt/python3/site-packages/torch_tensorrt/lib:/usr/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH} +# ENV PATH /opt/python3/site-packages/torch_tensorrt/bin:${PATH} -CMD /bin/bash +# CMD /bin/bash diff --git a/docker/README.md b/docker/README.md index 9f83f25134..24a76ea9ec 100644 --- a/docker/README.md +++ b/docker/README.md @@ -3,7 +3,7 @@ * Use `Dockerfile` to build a container which provides the exact development environment that our master branch is usually tested against. * The `Dockerfile` currently uses Bazelisk to select the Bazel version, and uses the exact library versions of Torch and CUDA listed in dependencies. - * The desired versions of CUDNN and TensorRT must be specified as build-args, with major and minor versions as in: `--build-arg TENSORRT_VERSION=a.b --build-arg CUDNN_VERSION=x.y` + * The desired version of TensorRT must be specified as build-args, with major and minor versions as in: `--build-arg TENSORRT_VERSION=a.b` * [**Optional**] The desired base image be changed by explicitly setting a base image, as in `--build-arg BASE_IMG=nvidia/cuda:11.8.0-devel-ubuntu22.04`, though this is optional * [**Optional**] Additionally, the desired Python version can be changed by explicitly setting a version, as in `--build-arg PYTHON_VERSION=3.10`, though this is optional as well. @@ -17,14 +17,14 @@ Note: By default the container uses the `pre-cxx11-abi` version of Torch + Torch ### Instructions -- The example below uses CUDNN 8.9 and TensorRT 8.6 +- The example below uses TensorRT 10.0.0.6 - See dependencies for a list of current default dependencies. > From root of Torch-TensorRT repo Build: ``` -DOCKER_BUILDKIT=1 docker build --build-arg TENSORRT_VERSION=8.6 --build-arg CUDNN_VERSION=8.9 -f docker/Dockerfile -t torch_tensorrt:latest . +DOCKER_BUILDKIT=1 docker build --build-arg TENSORRT_VERSION=10.0 -f docker/Dockerfile -t torch_tensorrt:latest . ``` Run: From 38642bb9ef8b4da63b25363cde751dc17a3812b9 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Thu, 18 Apr 2024 16:07:04 -0700 Subject: [PATCH 066/122] chore: updates --- py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py b/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py index 05808dd37c..9a75add755 100644 --- a/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py +++ b/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py @@ -313,7 +313,7 @@ def run( ) timing_cache = self._create_timing_cache(builder_config, existing_cache) - engine = self.builder.build_engine(self.ctx.net, builder_config) + engine = self.builder.build_serialized_network(self.ctx.net, builder_config) assert engine serialized_cache = ( From ba286bdab40c0d0aa418b609ef129306a555b241 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Thu, 18 Apr 2024 16:40:24 -0700 Subject: [PATCH 067/122] chore: add fp8 test --- tests/py/dynamo/models/test_models_export.py | 42 ++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/py/dynamo/models/test_models_export.py b/tests/py/dynamo/models/test_models_export.py index 38889a3df8..fe41cd1406 100644 --- a/tests/py/dynamo/models/test_models_export.py +++ b/tests/py/dynamo/models/test_models_export.py @@ -180,3 +180,45 @@ def test_resnet18_half(ir): # Clean up model env torch._dynamo.reset() + + +@pytest.mark.unit +def test_base_fp8(ir): + class SimpleNetwork(nn.Module): + def __init__(self): + super(SimpleNetwork, self).__init__() + self.linear1 = nn.Linear(in_features=10, out_features=5) + self.linear2 = nn.Linear(in_features=5, out_features=1) + + def forward(self, x): + x = self.linear1(x) + x = torch.nn.ReLU()(x) + x = self.linear2(x) + return x + + import ammo.torch.quantization as atq + from ammo.torch.quantization.torch_export import export_torch_mode + + def calibrate_loop(model): + """Simple calibration function for testing.""" + model(input_tensor) + + input_tensor = torch.randn(1, 10).cuda() + model = SimpleNetwork().eval().cuda() + + with torch.no_grad(): + quant_cfg = atq.FP8_DEFAULT_CFG + atq.quantize(model, quant_cfg, forward_loop=calibrate_loop) + # model has FP8 qdq nodes at this point + output_pyt = model(input_tensor) + + with export_torch_mode(): + exp_program = torch.export.export(model, (input_tensor,)) + trt_model = torch_tensorrt.dynamo.compile( + exp_program, + inputs=[input_tensor], + enabled_precisions={torch.float8_e4m3fn}, + min_block_size=1, + ) + outputs_trt = trt_model(input_tensor) + assert torch.allclose(output_pyt, outputs_trt, rtol=1e-3, atol=1e-2) From d15dd72c38e91ddc20f10b5452f76239e16f819a Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Thu, 18 Apr 2024 17:21:21 -0700 Subject: [PATCH 068/122] chore: updates --- py/torch_tensorrt/_enums.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/torch_tensorrt/_enums.py b/py/torch_tensorrt/_enums.py index 5c16cd03cd..062abb9a87 100644 --- a/py/torch_tensorrt/_enums.py +++ b/py/torch_tensorrt/_enums.py @@ -107,7 +107,7 @@ def _from( return dtype.f16 elif t == trt.float32: return dtype.f32 - elif trt.__version__ >= "7.0" and t == trt.bool: + elif t == trt.bool: return dtype.b else: raise TypeError( From dee9aa02d941be5312f7e3cceef5ad4f887aefe1 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Fri, 19 Apr 2024 09:23:09 -0700 Subject: [PATCH 069/122] chore: updates --- .github/workflows/build-test.yml | 1 + tests/py/dynamo/conversion/test_erf_aten.py | 2 +- tests/py/dynamo/conversion/test_neg_aten.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 97f4ad2ba0..f4d39bd056 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -264,6 +264,7 @@ jobs: pre-script: ${{ matrix.pre-script }} script: | export USE_HOST_DEPS=1 + export LD_LIBRARY_PATH=/opt/torch-tensorrt-builds/TensorRT-10.0.0.6/lib:$LD_LIBRARY_PATH pushd . cd tests/py/core ${CONDA_RUN} python -m pip install --pre pytest-xdist timm transformers parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver diff --git a/tests/py/dynamo/conversion/test_erf_aten.py b/tests/py/dynamo/conversion/test_erf_aten.py index d65788bc85..d9d201b0ae 100644 --- a/tests/py/dynamo/conversion/test_erf_aten.py +++ b/tests/py/dynamo/conversion/test_erf_aten.py @@ -22,7 +22,7 @@ def forward(self, input): return torch.ops.aten.erf.default(input) inputs = [torch.randn(x, dtype=type)] - self.run_test(erf(), inputs, precision=type, output_dtypes=[type]) + self.run_test(erf(), inputs, precision=type) @parameterized.expand( [ diff --git a/tests/py/dynamo/conversion/test_neg_aten.py b/tests/py/dynamo/conversion/test_neg_aten.py index 818d9c5a83..795a78354f 100644 --- a/tests/py/dynamo/conversion/test_neg_aten.py +++ b/tests/py/dynamo/conversion/test_neg_aten.py @@ -22,7 +22,7 @@ def forward(self, input): return torch.ops.aten.neg.default(input) inputs = [torch.randn(x, dtype=type)] - self.run_test(neg(), inputs, precision=type, output_dtypes=[type]) + self.run_test(neg(), inputs, precision=type) @parameterized.expand( [ From c05d675d5db3a5b12fee6e2505dfdc9a4b51ccaf Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Fri, 19 Apr 2024 16:25:01 -0700 Subject: [PATCH 070/122] chore: update stream in python runtime --- .../runtime/_PythonTorchTensorRTModule.py | 3 +-- tests/py/ts/models/hw_compat.ts | Bin 110418 -> 0 bytes 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 tests/py/ts/models/hw_compat.ts diff --git a/py/torch_tensorrt/dynamo/runtime/_PythonTorchTensorRTModule.py b/py/torch_tensorrt/dynamo/runtime/_PythonTorchTensorRTModule.py index ef24a57f8e..49ed64a953 100644 --- a/py/torch_tensorrt/dynamo/runtime/_PythonTorchTensorRTModule.py +++ b/py/torch_tensorrt/dynamo/runtime/_PythonTorchTensorRTModule.py @@ -59,7 +59,6 @@ def _initialize(self) -> None: runtime = trt.Runtime(logger) self.engine = runtime.deserialize_cuda_engine(self.engine) self.context = self.engine.create_execution_context() - self.stream = torch.cuda.Stream(torch.cuda.current_device()) # Indices of inputs/outputs in the trt engine bindings, in the order # as they are in the original PyTorch model. @@ -282,7 +281,7 @@ def forward(self, *inputs: torch.Tensor) -> torch.Tensor | Tuple[torch.Tensor, . if self.profiling_enabled else nullcontext() ): - self.context.execute_async_v3(self.stream.cuda_stream) + self.context.execute_async_v3(torch.cuda.current_stream().cuda_stream) if len(outputs) == 1: return outputs[0] diff --git a/tests/py/ts/models/hw_compat.ts b/tests/py/ts/models/hw_compat.ts deleted file mode 100644 index ed71663c0c77a406307a3e7c23d331c1b44b7243..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 110418 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

AVH5!7ggT8};I(-MYt2XYLLZw4h4CbNkR6|h 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

>FLVV@nOJ`|5%|y_n@H9JTy^kqv~$X1lXI;Z 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 From 23296573c4eb02b7687f44cd56da0658af6a5980 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Fri, 19 Apr 2024 16:42:55 -0700 Subject: [PATCH 071/122] chore: update hw_compat.ts --- tests/py/ts/models/hw_compat.ts | Bin 0 -> 110481 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/py/ts/models/hw_compat.ts diff --git a/tests/py/ts/models/hw_compat.ts b/tests/py/ts/models/hw_compat.ts new file mode 100644 index 0000000000000000000000000000000000000000..3cf583c78824407d066054b56da3095b37c2615d GIT binary patch literal 110481 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 Date: Fri, 19 Apr 2024 23:22:46 -0700 Subject: [PATCH 072/122] chore: updates --- tests/py/dynamo/lowering/test_aten_lowering_passes.py | 2 +- tests/py/dynamo/testing_utilities.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/py/dynamo/lowering/test_aten_lowering_passes.py b/tests/py/dynamo/lowering/test_aten_lowering_passes.py index 5c0a1bb774..2d7a4731f5 100644 --- a/tests/py/dynamo/lowering/test_aten_lowering_passes.py +++ b/tests/py/dynamo/lowering/test_aten_lowering_passes.py @@ -453,7 +453,7 @@ def forward(self, input, weight, bias): self.assertAlmostEqual( max_diff, 0, - DECIMALS_OF_AGREEMENT_3, + DECIMALS_OF_AGREEMENT, msg=f"Linear TRT outputs don't match with the original model.", ) torch._dynamo.reset() diff --git a/tests/py/dynamo/testing_utilities.py b/tests/py/dynamo/testing_utilities.py index c815d2fde4..742b9fc1a3 100644 --- a/tests/py/dynamo/testing_utilities.py +++ b/tests/py/dynamo/testing_utilities.py @@ -14,7 +14,6 @@ ) DECIMALS_OF_AGREEMENT = 4 -DECIMALS_OF_AGREEMENT_3 = 3 def fx_dynamo_testing_backend( From 44778e11f9feecad1f5123489911339c9a79bf99 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Fri, 19 Apr 2024 23:49:29 -0700 Subject: [PATCH 073/122] chore: updates --- cpp/include/torch_tensorrt/ptq.h | 8 ++++---- .../dynamo/runtime/_PythonTorchTensorRTModule.py | 5 ----- pyproject.toml | 2 ++ 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/cpp/include/torch_tensorrt/ptq.h b/cpp/include/torch_tensorrt/ptq.h index e88da06b0c..d8570f0e6e 100644 --- a/cpp/include/torch_tensorrt/ptq.h +++ b/cpp/include/torch_tensorrt/ptq.h @@ -21,10 +21,10 @@ #include "torch_tensorrt/macros.h" #ifndef DOXYGEN_SHOULD_SKIP_THIS -// namespace nvinfer1 { -// class IInt8Calibrator; -// class IInt8EntropyCalibrator2; -// } // namespace nvinfer1 +namespace nvinfer1 { +class IInt8Calibrator; +class IInt8EntropyCalibrator2; +} // namespace nvinfer1 namespace torch_tensorrt { namespace ptq { diff --git a/py/torch_tensorrt/dynamo/runtime/_PythonTorchTensorRTModule.py b/py/torch_tensorrt/dynamo/runtime/_PythonTorchTensorRTModule.py index 49ed64a953..7b82d41cb6 100644 --- a/py/torch_tensorrt/dynamo/runtime/_PythonTorchTensorRTModule.py +++ b/py/torch_tensorrt/dynamo/runtime/_PythonTorchTensorRTModule.py @@ -209,11 +209,6 @@ def forward(self, *inputs: torch.Tensor) -> torch.Tensor | Tuple[torch.Tensor, . contiguous_inputs: List[torch.Tensor] = [i.contiguous() for i in inputs] bindings = [] - # [None] * ( - # len(self.input_names) - # + len(self.output_names) - # # + len(self.hidden_output_names) # TODO: Verify if this is required - # ) for i, input_name in enumerate(self.input_names): if not contiguous_inputs[i].is_cuda: logger.warning( diff --git a/pyproject.toml b/pyproject.toml index 79e5391f52..ec6c0fe19c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ requires = [ "cffi>=1.15.1", "typing-extensions>=4.7.0", "future>=0.18.3", + "tensorrt", "torch==2.3.0", "pybind11==2.6.2", "numpy", @@ -41,6 +42,7 @@ requires-python = ">=3.8" keywords = ["pytorch", "torch", "tensorrt", "trt", "ai", "artificial intelligence", "ml", "machine learning", "dl", "deep learning", "compiler", "dynamo", "torchscript", "inference"] dependencies = [ "torch==2.3.0", + "tensorrt", "packaging>=23", "numpy", "typing-extensions>=4.7.0", From 0dbbcd71da88f437e7c87a040138fdfe51d23031 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Sat, 20 Apr 2024 01:22:00 -0700 Subject: [PATCH 074/122] chore: updates --- cpp/include/torch_tensorrt/ptq.h | 8 ++++---- packaging/pre_build_script.sh | 2 +- py/requirements.txt | 3 ++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/cpp/include/torch_tensorrt/ptq.h b/cpp/include/torch_tensorrt/ptq.h index d8570f0e6e..e88da06b0c 100644 --- a/cpp/include/torch_tensorrt/ptq.h +++ b/cpp/include/torch_tensorrt/ptq.h @@ -21,10 +21,10 @@ #include "torch_tensorrt/macros.h" #ifndef DOXYGEN_SHOULD_SKIP_THIS -namespace nvinfer1 { -class IInt8Calibrator; -class IInt8EntropyCalibrator2; -} // namespace nvinfer1 +// namespace nvinfer1 { +// class IInt8Calibrator; +// class IInt8EntropyCalibrator2; +// } // namespace nvinfer1 namespace torch_tensorrt { namespace ptq { diff --git a/packaging/pre_build_script.sh b/packaging/pre_build_script.sh index befb6e38d9..8f5d1d8acc 100755 --- a/packaging/pre_build_script.sh +++ b/packaging/pre_build_script.sh @@ -10,7 +10,7 @@ export LD_LIBRARY_PATH=/opt/torch-tensorrt-builds/TensorRT-10.0.0.6/lib:$LD_LIBR wget https://github.com/bazelbuild/bazelisk/releases/download/v1.17.0/bazelisk-linux-amd64 \ && mv bazelisk-linux-amd64 /usr/bin/bazel \ && chmod +x /usr/bin/bazel -python -m pip install -r py/requirements.txt + export TORCH_BUILD_NUMBER=$(python -c "import torch, urllib.parse as ul; print(ul.quote_plus(torch.__version__))") cat toolchains/ci_workspaces/WORKSPACE.x86_64.release.rhel.tmpl | envsubst > WORKSPACE diff --git a/py/requirements.txt b/py/requirements.txt index e3356a8c59..d402fd501e 100644 --- a/py/requirements.txt +++ b/py/requirements.txt @@ -5,4 +5,5 @@ pybind11==2.6.2 torch==2.3.0 torchvision==0.18.0 --extra-index-url https://pypi.ngc.nvidia.com -pyyaml \ No newline at end of file +pyyaml +tensorrt \ No newline at end of file From 89c3d76b7e94a8f37df8937f0552f75633b3445e Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 22 Apr 2024 16:01:37 -0700 Subject: [PATCH 075/122] chore: updates --- core/runtime/TRTEngine.cpp | 2 + cpp/include/torch_tensorrt/ptq.h | 5 -- .../runtime/_PythonTorchTensorRTModule.py | 53 ++----------------- 3 files changed, 6 insertions(+), 54 deletions(-) diff --git a/core/runtime/TRTEngine.cpp b/core/runtime/TRTEngine.cpp index fcbb403e2c..4a33907bec 100644 --- a/core/runtime/TRTEngine.cpp +++ b/core/runtime/TRTEngine.cpp @@ -133,6 +133,7 @@ TRTEngine::TRTEngine( trt_idx = idx; if (cuda_engine->getTensorIOMode(binding_name.c_str()) == nvinfer1::TensorIOMode::kINPUT) { is_input = true; + break; } } } @@ -161,6 +162,7 @@ TRTEngine::TRTEngine( trt_idx = idx; if (cuda_engine->getTensorIOMode(binding_name.c_str()) == nvinfer1::TensorIOMode::kOUTPUT) { is_output = true; + break; } } } diff --git a/cpp/include/torch_tensorrt/ptq.h b/cpp/include/torch_tensorrt/ptq.h index e88da06b0c..6650f45fe9 100644 --- a/cpp/include/torch_tensorrt/ptq.h +++ b/cpp/include/torch_tensorrt/ptq.h @@ -21,11 +21,6 @@ #include "torch_tensorrt/macros.h" #ifndef DOXYGEN_SHOULD_SKIP_THIS -// namespace nvinfer1 { -// class IInt8Calibrator; -// class IInt8EntropyCalibrator2; -// } // namespace nvinfer1 - namespace torch_tensorrt { namespace ptq { TORCHTRT_API bool get_batch_impl(void* bindings[], const char* names[], int nbBindings, torch::Tensor& data); diff --git a/py/torch_tensorrt/dynamo/runtime/_PythonTorchTensorRTModule.py b/py/torch_tensorrt/dynamo/runtime/_PythonTorchTensorRTModule.py index 7b82d41cb6..0c152e15f1 100644 --- a/py/torch_tensorrt/dynamo/runtime/_PythonTorchTensorRTModule.py +++ b/py/torch_tensorrt/dynamo/runtime/_PythonTorchTensorRTModule.py @@ -15,6 +15,7 @@ _select_rt_device, multi_gpu_device_check, ) +from torch_tensorrt.logging import TRT_LOGGER logger = logging.getLogger(__name__) @@ -55,39 +56,13 @@ def __init__( def _initialize(self) -> None: self.initialized = True - logger = trt.Logger() - runtime = trt.Runtime(logger) + runtime = trt.Runtime(TRT_LOGGER) self.engine = runtime.deserialize_cuda_engine(self.engine) self.context = self.engine.create_execution_context() - # Indices of inputs/outputs in the trt engine bindings, in the order - # as they are in the original PyTorch model. - - # TODO: Verify if the following is required especially the hidden outputs - # self.input_binding_indices_in_order: Sequence[int] = [ - # self.engine.get_binding_index(name) for name in self.input_names - # ] - # self.output_binding_indices_in_order: Sequence[int] = [ - # self.engine.get_binding_index(name) for name in self.output_names - # ] - # primary_input_outputs = set() - # primary_input_outputs.update(self.input_binding_indices_in_order) - # primary_input_outputs.update(self.output_binding_indices_in_order) - # self.hidden_output_binding_indices_in_order: Sequence[int] = [] - # self.hidden_output_names: Sequence[str] = [] - # for i in range( - # self.engine.num_bindings // self.engine.num_optimization_profiles - # ): - # if i not in primary_input_outputs: - # self.hidden_output_binding_indices_in_order.append(i) - # self.hidden_output_names.append(self.engine.get_binding_name(i)) assert ( self.engine.num_io_tensors // self.engine.num_optimization_profiles - ) == ( - len(self.input_names) - + len(self.output_names) - # + len(self.hidden_output_names) #TODO: Verify if this is required - ) + ) == (len(self.input_names) + len(self.output_names)) self.input_dtypes = [ dtype._from(self.engine.get_tensor_dtype(input_name)) @@ -105,14 +80,6 @@ def _initialize(self) -> None: for output_name in self.output_names ] - # TODO: Verify what this is for ? - # self.hidden_output_dtypes = [ - # unified_dtype_converter( - # self.engine.get_binding_dtype(idx), Frameworks.TORCH - # ) - # for idx in self.hidden_output_binding_indices_in_order - # ] - def _check_initialized(self) -> None: if not self.initialized: raise RuntimeError("PythonTorchTensorRTModule is not initialized.") @@ -138,8 +105,7 @@ def _load_from_state_dict( # Run multi-gpu device check to validate engine instantiation multi_gpu_device_check() - logger = trt.Logger() - runtime = trt.Runtime(logger) + runtime = trt.Runtime(TRT_LOGGER) self.engine = runtime.deserialize_cuda_engine(engine_bytes) self.input_names = state_dict[prefix + "input_names"] @@ -253,16 +219,6 @@ def forward(self, *inputs: torch.Tensor) -> torch.Tensor | Tuple[torch.Tensor, . bindings.append(output.data_ptr()) outputs.append(output) - # TODO: Check what is this for ? - # for i, idx in enumerate(self.hidden_output_binding_indices_in_order): - # shape = tuple(self.context.get_binding_shape(idx)) - - # output = torch.empty( - # size=shape, - # dtype=self.hidden_output_dtypes[i], - # device=torch.cuda.current_device(), - # ) - # Assign tensor address appropriately for idx in range(self.engine.num_io_tensors): self.context.set_tensor_address( @@ -300,7 +256,6 @@ def disable_profiling(self) -> None: Disable TensorRT profiling. """ self._check_initialized() - torch.cuda.synchronize() del self.context self.context = self.engine.create_execution_context() From 3956749712d3b9e249f1fac438e67460add1de44 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 22 Apr 2024 17:41:22 -0700 Subject: [PATCH 076/122] chore: updates --- py/torch_tensorrt/dynamo/_defaults.py | 2 +- tests/py/dynamo/models/test_models_export.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/py/torch_tensorrt/dynamo/_defaults.py b/py/torch_tensorrt/dynamo/_defaults.py index 27db215466..c9b8be6bea 100644 --- a/py/torch_tensorrt/dynamo/_defaults.py +++ b/py/torch_tensorrt/dynamo/_defaults.py @@ -26,7 +26,7 @@ REQUIRE_FULL_COMPILATION = False DRYRUN = False HARDWARE_COMPATIBLE = False -SUPPORTED_KERNEL_PRECISIONS = {dtype.f32, dtype.f16, dtype.i8} +SUPPORTED_KERNEL_PRECISIONS = {dtype.f32, dtype.f16, dtype.i8, dtype.f8} def default_device() -> Device: diff --git a/tests/py/dynamo/models/test_models_export.py b/tests/py/dynamo/models/test_models_export.py index fe41cd1406..d3ab46725d 100644 --- a/tests/py/dynamo/models/test_models_export.py +++ b/tests/py/dynamo/models/test_models_export.py @@ -182,6 +182,10 @@ def test_resnet18_half(ir): torch._dynamo.reset() +@unittest.skipIf( + not torch.cuda.get_device_properties(torch.cuda.current_device()).major < 9, + "FP8 compilation in Torch-TRT is not supported on cards older than Hopper", +) @pytest.mark.unit def test_base_fp8(ir): class SimpleNetwork(nn.Module): From 0a2337b1d2d9671578e0e2e04042b506a2b4ad5f Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 22 Apr 2024 17:48:31 -0700 Subject: [PATCH 077/122] chore: updates --- .github/workflows/build-test.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index f4d39bd056..4d123ea8b8 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -66,6 +66,7 @@ jobs: package-name: torch_tensorrt pre-script: packaging/pre_build_script.sh post-script: packaging/post_build_script.sh + smoke-test-script: packaging/smoke_test_script.sh uses: pytorch/tensorrt/.github/workflows/linux-test.yml@release/2.3 with: job-name: tests-py-torchscript-fe @@ -104,6 +105,7 @@ jobs: package-name: torch_tensorrt pre-script: packaging/pre_build_script.sh post-script: packaging/post_build_script.sh + smoke-test-script: packaging/smoke_test_script.sh uses: pytorch/tensorrt/.github/workflows/linux-test.yml@release/2.3 with: job-name: tests-py-dynamo-converters @@ -133,6 +135,7 @@ jobs: package-name: torch_tensorrt pre-script: packaging/pre_build_script.sh post-script: packaging/post_build_script.sh + smoke-test-script: packaging/smoke_test_script.sh uses: pytorch/tensorrt/.github/workflows/linux-test.yml@release/2.3 with: job-name: tests-py-dynamo-fe @@ -163,6 +166,7 @@ jobs: package-name: torch_tensorrt pre-script: packaging/pre_build_script.sh post-script: packaging/post_build_script.sh + smoke-test-script: packaging/smoke_test_script.sh uses: pytorch/tensorrt/.github/workflows/linux-test.yml@release/2.3 with: job-name: tests-py-dynamo-serde @@ -192,6 +196,7 @@ jobs: package-name: torch_tensorrt pre-script: packaging/pre_build_script.sh post-script: packaging/post_build_script.sh + smoke-test-script: packaging/smoke_test_script.sh uses: pytorch/tensorrt/.github/workflows/linux-test.yml@release/2.3 with: job-name: tests-py-torch-compile-be @@ -223,6 +228,7 @@ jobs: package-name: torch_tensorrt pre-script: packaging/pre_build_script.sh post-script: packaging/post_build_script.sh + smoke-test-script: packaging/smoke_test_script.sh uses: pytorch/tensorrt/.github/workflows/linux-test.yml@release/2.3 with: job-name: tests-py-dynamo-core @@ -253,6 +259,8 @@ jobs: - repository: pytorch/tensorrt package-name: torch_tensorrt pre-script: packaging/pre_build_script.sh + post-script: packaging/post_build_script.sh + smoke-test-script: packaging/smoke_test_script.sh uses: pytorch/tensorrt/.github/workflows/linux-test.yml@main with: job-name: tests-py-core From 358255df0ee9293b95274948ced696251c2148be Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 22 Apr 2024 17:49:47 -0700 Subject: [PATCH 078/122] chore: updates --- .github/workflows/build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 4d123ea8b8..5121302036 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -261,7 +261,7 @@ jobs: pre-script: packaging/pre_build_script.sh post-script: packaging/post_build_script.sh smoke-test-script: packaging/smoke_test_script.sh - uses: pytorch/tensorrt/.github/workflows/linux-test.yml@main + uses: pytorch/tensorrt/.github/workflows/linux-test.yml@release/2.3 with: job-name: tests-py-core repository: "pytorch/tensorrt" From 7e717d63cf60f54f0d9505a0af7d37adb012c744 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 14 May 2024 10:42:53 -0700 Subject: [PATCH 079/122] chore: updates --- tests/py/dynamo/conversion/harness.py | 19 ++++++++++--------- tests/py/dynamo/models/test_models_export.py | 4 ++-- tests/py/dynamo/testing_utilities.py | 4 ++-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/tests/py/dynamo/conversion/harness.py b/tests/py/dynamo/conversion/harness.py index 7ce3939371..4825e45d26 100644 --- a/tests/py/dynamo/conversion/harness.py +++ b/tests/py/dynamo/conversion/harness.py @@ -14,7 +14,7 @@ # Use interpreter, input spec, and test case from fx_ts_compat to test Dynamo Converter Registry from torch_tensorrt.dynamo.conversion import TRTInterpreter from torch_tensorrt.dynamo.conversion._conversion import infer_module_output_dtypes -from torch_tensorrt.dynamo.lowering import apply_lowering_passes +from torch_tensorrt.dynamo.lowering import post_lowering, pre_export_lowering from torch_tensorrt.dynamo.runtime import PythonTorchTensorRTModule _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -200,17 +200,18 @@ def generate_graph( enable_passes: bool, ): if use_dynamo_tracer: - fx_module = torch._dynamo.export( - mod, - *original_inputs, - aten_graph=True, - assume_static_by_default=True, - tracing_mode="real", - ).graph_module + exported_program = torch.export.export(mod, tuple(original_inputs)) + exported_program = pre_export_lowering(exported_program, original_inputs) + exported_program = exported_program.run_decompositions( + get_decompositions(False) + ) + fx_module = exported_program.module() else: fx_module = torch.fx.symbolic_trace(mod) + if enable_passes: - fx_module = apply_lowering_passes(fx_module, original_inputs) + fx_module = post_lowering(fx_module, original_inputs) + return fx_module def run_test( diff --git a/tests/py/dynamo/models/test_models_export.py b/tests/py/dynamo/models/test_models_export.py index dda466b048..79d6f8a42c 100644 --- a/tests/py/dynamo/models/test_models_export.py +++ b/tests/py/dynamo/models/test_models_export.py @@ -193,8 +193,8 @@ def test_base_fp8(ir): class SimpleNetwork(nn.Module): def __init__(self): super(SimpleNetwork, self).__init__() - self.linear1 = nn.Linear(in_features=10, out_features=5) - self.linear2 = nn.Linear(in_features=5, out_features=1) + self.linear1 = torch.nn.Linear(in_features=10, out_features=5) + self.linear2 = torch.nn.Linear(in_features=5, out_features=1) def forward(self, x): x = self.linear1(x) diff --git a/tests/py/dynamo/testing_utilities.py b/tests/py/dynamo/testing_utilities.py index 742b9fc1a3..e0112ae523 100644 --- a/tests/py/dynamo/testing_utilities.py +++ b/tests/py/dynamo/testing_utilities.py @@ -8,8 +8,8 @@ from torch._functorch.aot_autograd import aot_export_joint_simple from torch_tensorrt.dynamo import partitioning from torch_tensorrt.dynamo.lowering import ( - apply_lowering_passes, get_decompositions, + post_lowering, repair_input_aliasing, ) @@ -50,7 +50,7 @@ def fx_dynamo_testing_backend( decompositions=get_decompositions(), ) - gm = apply_lowering_passes(gm, sample_inputs) + gm = post_lowering(gm, sample_inputs) trt_compiled = custom_backend( gm, From c6d2f2ad84e25613f02d8cdbb5a864d65372433b Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 14 May 2024 11:20:50 -0700 Subject: [PATCH 080/122] chore: update to modelopt --- tests/py/dynamo/models/test_models_export.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/py/dynamo/models/test_models_export.py b/tests/py/dynamo/models/test_models_export.py index 79d6f8a42c..7bc9731147 100644 --- a/tests/py/dynamo/models/test_models_export.py +++ b/tests/py/dynamo/models/test_models_export.py @@ -202,8 +202,8 @@ def forward(self, x): x = self.linear2(x) return x - import ammo.torch.quantization as atq - from ammo.torch.quantization.torch_export import export_torch_mode + import modelopt.torch.quantization as mtq + from modelopt.torch.quantization.utils import export_torch_mode def calibrate_loop(model): """Simple calibration function for testing.""" @@ -212,12 +212,12 @@ def calibrate_loop(model): input_tensor = torch.randn(1, 10).cuda() model = SimpleNetwork().eval().cuda() - with torch.no_grad(): - quant_cfg = atq.FP8_DEFAULT_CFG - atq.quantize(model, quant_cfg, forward_loop=calibrate_loop) - # model has FP8 qdq nodes at this point - output_pyt = model(input_tensor) + quant_cfg = mtq.FP8_DEFAULT_CFG + mtq.quantize(model, quant_cfg, forward_loop=calibrate_loop) + # model has FP8 qdq nodes at this point + output_pyt = model(input_tensor) + with torch.no_grad(): with export_torch_mode(): exp_program = torch.export.export(model, (input_tensor,)) trt_model = torch_tensorrt.dynamo.compile( From ceec39dc2b47aced90713e4c6032d3fe4fad15a8 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 14 May 2024 12:48:54 -0700 Subject: [PATCH 081/122] chore: updates --- tests/py/dynamo/conversion/harness.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/py/dynamo/conversion/harness.py b/tests/py/dynamo/conversion/harness.py index 4825e45d26..df95911065 100644 --- a/tests/py/dynamo/conversion/harness.py +++ b/tests/py/dynamo/conversion/harness.py @@ -14,7 +14,11 @@ # Use interpreter, input spec, and test case from fx_ts_compat to test Dynamo Converter Registry from torch_tensorrt.dynamo.conversion import TRTInterpreter from torch_tensorrt.dynamo.conversion._conversion import infer_module_output_dtypes -from torch_tensorrt.dynamo.lowering import post_lowering, pre_export_lowering +from torch_tensorrt.dynamo.lowering import ( + get_decompositions, + post_lowering, + pre_export_lowering, +) from torch_tensorrt.dynamo.runtime import PythonTorchTensorRTModule _LOGGER: logging.Logger = logging.getLogger(__name__) From a7e566b071b6f09bc56ce81f482def9603e1b928 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 14 May 2024 13:29:33 -0700 Subject: [PATCH 082/122] chore: updates --- py/torch_tensorrt/dynamo/_compiler.py | 1 - .../dynamo/lowering/_remove_sym_nodes.py | 5 +++-- .../dynamo/lowering/passes/fuse_prims_broadcast.py | 11 ----------- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/py/torch_tensorrt/dynamo/_compiler.py b/py/torch_tensorrt/dynamo/_compiler.py index 28480418d3..540caad3b1 100644 --- a/py/torch_tensorrt/dynamo/_compiler.py +++ b/py/torch_tensorrt/dynamo/_compiler.py @@ -178,7 +178,6 @@ def compile( f"Input graph should be an ExportedProgram but got type {type(exported_program)}" ) - exported_program = pre_export_lowering(exported_program, torch_inputs) exported_program = exported_program.run_decompositions( get_decompositions(enable_experimental_decompositions) ) diff --git a/py/torch_tensorrt/dynamo/lowering/_remove_sym_nodes.py b/py/torch_tensorrt/dynamo/lowering/_remove_sym_nodes.py index e85117a423..8adebc87f8 100644 --- a/py/torch_tensorrt/dynamo/lowering/_remove_sym_nodes.py +++ b/py/torch_tensorrt/dynamo/lowering/_remove_sym_nodes.py @@ -10,17 +10,18 @@ def remove_sym_nodes(gm: torch.fx.GraphModule) -> torch.fx.GraphModule: dynamic=True behavior """ # Extract SymInt placeholder Tensors - placeholders = [ + placeholder_sym_ints = [ node for node in gm.graph.nodes if ( node.op == "placeholder" and isinstance(node.type, type) and issubclass(node.type, torch.SymInt) + and not node.users ) ] - for node in placeholders: + for node in placeholder_sym_ints: gm.graph.erase_node(node) gm.graph.lint() diff --git a/py/torch_tensorrt/dynamo/lowering/passes/fuse_prims_broadcast.py b/py/torch_tensorrt/dynamo/lowering/passes/fuse_prims_broadcast.py index 312926e870..aa7403f94e 100644 --- a/py/torch_tensorrt/dynamo/lowering/passes/fuse_prims_broadcast.py +++ b/py/torch_tensorrt/dynamo/lowering/passes/fuse_prims_broadcast.py @@ -2,7 +2,6 @@ from typing import Sequence import torch -from torch.fx.passes.shape_prop import ShapeProp from torch_tensorrt.dynamo.lowering.passes.pass_utils import ( clean_up_graph_after_modifications, ) @@ -17,16 +16,6 @@ def fuse_prims_broadcast( """Fuses prim nodes which are effectively the ATen equivalents with keep_dim=True""" modified_graph = False - # Propagate shapes through the graph to determine if broadcast can be resolved - try: - ShapeProp(gm).propagate(*sample_inputs) - except (RuntimeError, AssertionError): - logger.warning( - "Shape Propagation Failed on Graph, skipping fuse_prims_broadcast lowering pass", - exc_info=True, - ) - return gm - for node in gm.graph.nodes: # If the node is a sum prims operator, with broadcast_in_dim being the only consumer # it is a candidate for fusing From 707b10ab4a1b9284ccad6a0f649fdff350f94eb7 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 14 May 2024 21:37:26 -0700 Subject: [PATCH 083/122] chore: updates --- tests/py/dynamo/conversion/harness.py | 2 +- tests/py/dynamo/models/test_models_export.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/py/dynamo/conversion/harness.py b/tests/py/dynamo/conversion/harness.py index df95911065..f877871fc4 100644 --- a/tests/py/dynamo/conversion/harness.py +++ b/tests/py/dynamo/conversion/harness.py @@ -65,7 +65,6 @@ def run_test( for i in inputs: cuda_inputs.append(i.cuda()) - mod.eval() start = time.perf_counter() interpreter_result = interpreter.run() sec = time.perf_counter() - start @@ -203,6 +202,7 @@ def generate_graph( use_dynamo_tracer: bool, enable_passes: bool, ): + mod = mod.eval() if use_dynamo_tracer: exported_program = torch.export.export(mod, tuple(original_inputs)) exported_program = pre_export_lowering(exported_program, original_inputs) diff --git a/tests/py/dynamo/models/test_models_export.py b/tests/py/dynamo/models/test_models_export.py index 7bc9731147..b6c82a43c3 100644 --- a/tests/py/dynamo/models/test_models_export.py +++ b/tests/py/dynamo/models/test_models_export.py @@ -190,7 +190,7 @@ def test_resnet18_half(ir): ) @pytest.mark.unit def test_base_fp8(ir): - class SimpleNetwork(nn.Module): + class SimpleNetwork(torch.nn.Module): def __init__(self): super(SimpleNetwork, self).__init__() self.linear1 = torch.nn.Linear(in_features=10, out_features=5) From 22066c52dfa8f0a3fdb586af2ec8c813041cce78 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 14 May 2024 23:29:43 -0700 Subject: [PATCH 084/122] chore: minor fix --- tests/py/dynamo/models/test_models_export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/py/dynamo/models/test_models_export.py b/tests/py/dynamo/models/test_models_export.py index b6c82a43c3..af9abc41f2 100644 --- a/tests/py/dynamo/models/test_models_export.py +++ b/tests/py/dynamo/models/test_models_export.py @@ -185,7 +185,7 @@ def test_resnet18_half(ir): @unittest.skipIf( - not torch.cuda.get_device_properties(torch.cuda.current_device()).major < 9, + torch.cuda.get_device_properties(torch.cuda.current_device()).major < 9, "FP8 compilation in Torch-TRT is not supported on cards older than Hopper", ) @pytest.mark.unit From 6eed3832f2fe5bdd66489eea95dccdb4ab63e3bc Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Wed, 15 May 2024 09:39:42 -0700 Subject: [PATCH 085/122] chore: fixes --- py/torch_tensorrt/dynamo/_tracer.py | 10 +++------- py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py | 2 -- tests/py/dynamo/conversion/harness.py | 7 +++---- tests/py/dynamo/conversion/test_bitwise_and_aten.py | 3 +++ tests/py/dynamo/conversion/test_bitwise_not_aten.py | 1 + tests/py/dynamo/conversion/test_bitwise_or_aten.py | 3 +++ tests/py/dynamo/conversion/test_bitwise_xor_aten.py | 3 +++ tests/py/dynamo/conversion/test_convolution_aten.py | 5 ++--- tests/py/dynamo/conversion/test_embedding_bag_aten.py | 3 +++ 9 files changed, 21 insertions(+), 16 deletions(-) diff --git a/py/torch_tensorrt/dynamo/_tracer.py b/py/torch_tensorrt/dynamo/_tracer.py index 11c0f6b3ac..6bc334f427 100644 --- a/py/torch_tensorrt/dynamo/_tracer.py +++ b/py/torch_tensorrt/dynamo/_tracer.py @@ -58,13 +58,9 @@ def trace( device = to_torch_device(kwargs.get("device", default_device())) torch_inputs = get_torch_inputs(inputs, device) - dynamic_shapes = {} + dynamic_shapes = [] for input in inputs: if isinstance(input, Input) and input.shape_mode == Input._ShapeMode.DYNAMIC: - if not input.name: - raise AssertionError( - f"Expected a name for a dynamic input with shape {input.shape} but found none" - ) min_shape = input.shape["min_shape"] opt_shape = input.shape["opt_shape"] max_shape = input.shape["max_shape"] @@ -80,8 +76,8 @@ def trace( max=max_shape[dim], ) - dynamic_shapes[input.name] = dynamic_dims + dynamic_shapes.append(dynamic_dims) - exp_program = export(mod, tuple(torch_inputs), dynamic_shapes=dynamic_shapes) + exp_program = export(mod, tuple(torch_inputs), dynamic_shapes=tuple(dynamic_shapes)) return exp_program diff --git a/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py b/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py index d541282fa5..c1a41602ef 100644 --- a/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py +++ b/py/torch_tensorrt/dynamo/conversion/_TRTInterpreter.py @@ -105,8 +105,6 @@ def __init__( [dtype._from(o) for o in output_dtypes] if output_dtypes else None ) - _LOGGER.debug(f"Graph to be compiled to TensorRT: {self.module.graph}") - def validate_conversion(self) -> Set[str]: missing_converters: Set[str] = set() diff --git a/tests/py/dynamo/conversion/harness.py b/tests/py/dynamo/conversion/harness.py index f877871fc4..250d41a566 100644 --- a/tests/py/dynamo/conversion/harness.py +++ b/tests/py/dynamo/conversion/harness.py @@ -6,6 +6,7 @@ from typing import Callable, List, Optional, Set, Tuple import torch +import torch_tensorrt from torch.testing._internal.common_utils import TestCase from torch_tensorrt import Input from torch_tensorrt._enums import dtype @@ -204,8 +205,7 @@ def generate_graph( ): mod = mod.eval() if use_dynamo_tracer: - exported_program = torch.export.export(mod, tuple(original_inputs)) - exported_program = pre_export_lowering(exported_program, original_inputs) + exported_program = torch_tensorrt.dynamo.trace(mod, tuple(original_inputs)) exported_program = exported_program.run_decompositions( get_decompositions(False) ) @@ -286,10 +286,9 @@ def run_test_with_dynamic_shape( enable_passes=False, ): mod.eval() - inputs = [spec.example_tensor("opt_shape") for spec in input_specs] mod = self.generate_graph( mod, - inputs, + input_specs, use_dynamo_tracer=use_dynamo_tracer, enable_passes=enable_passes, ) diff --git a/tests/py/dynamo/conversion/test_bitwise_and_aten.py b/tests/py/dynamo/conversion/test_bitwise_and_aten.py index 5c2a78a18a..8e7d8cef73 100644 --- a/tests/py/dynamo/conversion/test_bitwise_and_aten.py +++ b/tests/py/dynamo/conversion/test_bitwise_and_aten.py @@ -26,6 +26,7 @@ def forward(self, lhs_val, rhs_val): bitwise_and(), inputs, enable_passes=True, + use_dynamo_tracer=True, ) @parameterized.expand( @@ -46,6 +47,7 @@ def forward(self, tensor): bitwise_and(), inputs, enable_passes=True, + use_dynamo_tracer=True, ) @parameterized.expand( @@ -66,6 +68,7 @@ def forward(self, tensor): bitwise_and(), inputs, enable_passes=True, + use_dynamo_tracer=True, ) diff --git a/tests/py/dynamo/conversion/test_bitwise_not_aten.py b/tests/py/dynamo/conversion/test_bitwise_not_aten.py index b811f1e51a..33d8629aff 100644 --- a/tests/py/dynamo/conversion/test_bitwise_not_aten.py +++ b/tests/py/dynamo/conversion/test_bitwise_not_aten.py @@ -25,6 +25,7 @@ def forward(self, val): bitwise_not(), inputs, enable_passes=True, + use_dynamo_tracer=True, ) diff --git a/tests/py/dynamo/conversion/test_bitwise_or_aten.py b/tests/py/dynamo/conversion/test_bitwise_or_aten.py index b5e0200734..e912a9c473 100644 --- a/tests/py/dynamo/conversion/test_bitwise_or_aten.py +++ b/tests/py/dynamo/conversion/test_bitwise_or_aten.py @@ -26,6 +26,7 @@ def forward(self, lhs_val, rhs_val): bitwise_or(), inputs, enable_passes=True, + use_dynamo_tracer=True, ) @parameterized.expand( @@ -46,6 +47,7 @@ def forward(self, tensor): bitwise_or(), inputs, enable_passes=True, + use_dynamo_tracer=True, ) @parameterized.expand( @@ -66,6 +68,7 @@ def forward(self, tensor): bitwise_or(), inputs, enable_passes=True, + use_dynamo_tracer=True, ) diff --git a/tests/py/dynamo/conversion/test_bitwise_xor_aten.py b/tests/py/dynamo/conversion/test_bitwise_xor_aten.py index 8c1a8136ef..4bd2790bf9 100644 --- a/tests/py/dynamo/conversion/test_bitwise_xor_aten.py +++ b/tests/py/dynamo/conversion/test_bitwise_xor_aten.py @@ -26,6 +26,7 @@ def forward(self, lhs_val, rhs_val): bitwise_xor(), inputs, enable_passes=True, + use_dynamo_tracer=True, ) @parameterized.expand( @@ -46,6 +47,7 @@ def forward(self, tensor): bitwise_xor(), inputs, enable_passes=True, + use_dynamo_tracer=True, ) @parameterized.expand( @@ -66,6 +68,7 @@ def forward(self, tensor): bitwise_xor(), inputs, enable_passes=True, + use_dynamo_tracer=True, ) diff --git a/tests/py/dynamo/conversion/test_convolution_aten.py b/tests/py/dynamo/conversion/test_convolution_aten.py index 7d69c871a9..95f4de92b5 100644 --- a/tests/py/dynamo/conversion/test_convolution_aten.py +++ b/tests/py/dynamo/conversion/test_convolution_aten.py @@ -1,7 +1,6 @@ import torch from parameterized import param, parameterized from torch.testing._internal.common_utils import run_tests - from torch_tensorrt import Input from .harness import DispatchTestCase @@ -138,7 +137,7 @@ def forward(self, x): Input( shape=(-1, 3, -1, -1), dtype=torch.float32, - shape_ranges=[((1, 3, 1, 1), (1, 3, 4, 4), (32, 3, 128, 128))], + shape_ranges=[((1, 3, 1, 1), (2, 3, 4, 4), (32, 3, 128, 128))], ), ] self.run_test_with_dynamic_shape( @@ -201,7 +200,7 @@ def forward(self, x): Input( shape=(-1, 3, -1, -1, -1), dtype=torch.float32, - shape_ranges=[((1, 3, 1, 1, 1), (1, 3, 4, 4, 4), (8, 3, 32, 32, 32))], + shape_ranges=[((1, 3, 1, 1, 1), (2, 3, 4, 4, 4), (8, 3, 32, 32, 32))], ), ] self.run_test_with_dynamic_shape( diff --git a/tests/py/dynamo/conversion/test_embedding_bag_aten.py b/tests/py/dynamo/conversion/test_embedding_bag_aten.py index 2154937b43..0e4d6c806f 100644 --- a/tests/py/dynamo/conversion/test_embedding_bag_aten.py +++ b/tests/py/dynamo/conversion/test_embedding_bag_aten.py @@ -144,6 +144,7 @@ def forward(self, weight, indices): inputs=[weight, indices], precision=weight.dtype, enable_passes=True, + use_dynamo_tracer=True, ) @parameterized.expand( @@ -340,6 +341,7 @@ def forward(self, weight, indices, offsets): inputs=[weight, indices, offsets], precision=weight.dtype, enable_passes=True, + use_dynamo_tracer=True, ) @parameterized.expand( @@ -403,6 +405,7 @@ def forward(self, weight, indices, offsets): inputs=[weight, indices, offsets], precision=weight.dtype, enable_passes=True, + use_dynamo_tracer=True, ) From ff231b5dd766c811c0023fb9d22186e884bce7f9 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Wed, 15 May 2024 23:44:24 -0700 Subject: [PATCH 086/122] chore: fixes --- tests/py/dynamo/conversion/harness.py | 23 +++++++++++++++---- .../conversion/test_deconvolution_aten.py | 5 ++-- .../conversion/test_embedding_bag_aten.py | 6 ++--- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/tests/py/dynamo/conversion/harness.py b/tests/py/dynamo/conversion/harness.py index 250d41a566..3f38824c55 100644 --- a/tests/py/dynamo/conversion/harness.py +++ b/tests/py/dynamo/conversion/harness.py @@ -7,20 +7,19 @@ import torch import torch_tensorrt +from torch.fx.passes.shape_prop import ShapeProp from torch.testing._internal.common_utils import TestCase from torch_tensorrt import Input from torch_tensorrt._enums import dtype +from torch_tensorrt.dynamo import _defaults from torch_tensorrt.dynamo._settings import CompilationSettings # Use interpreter, input spec, and test case from fx_ts_compat to test Dynamo Converter Registry from torch_tensorrt.dynamo.conversion import TRTInterpreter from torch_tensorrt.dynamo.conversion._conversion import infer_module_output_dtypes -from torch_tensorrt.dynamo.lowering import ( - get_decompositions, - post_lowering, - pre_export_lowering, -) +from torch_tensorrt.dynamo.lowering import get_decompositions, post_lowering from torch_tensorrt.dynamo.runtime import PythonTorchTensorRTModule +from torch_tensorrt.dynamo.utils import get_torch_inputs _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -202,6 +201,7 @@ def generate_graph( original_inputs: List[torch.Tensor], use_dynamo_tracer: bool, enable_passes: bool, + propagate_shapes: bool, ): mod = mod.eval() if use_dynamo_tracer: @@ -216,6 +216,17 @@ def generate_graph( if enable_passes: fx_module = post_lowering(fx_module, original_inputs) + if propagate_shapes: + # TODO: This is currently being used to test embedding_bag_aten due to https://github.com/pytorch/TensorRT/issues/2843 + torch_inputs = get_torch_inputs(original_inputs, _defaults.DEVICE) + try: + ShapeProp(fx_module).propagate(*torch_inputs) + except (RuntimeError, AssertionError): + logger.warning( + "Shape Propagation failed on Graph, skipping it", + exc_info=False, + ) + return fx_module def run_test( @@ -228,6 +239,7 @@ def run_test( check_dtype=True, use_dynamo_tracer=False, enable_passes=False, + propagate_shapes=False, ): mod.eval() mod = self.generate_graph( @@ -235,6 +247,7 @@ def run_test( inputs, use_dynamo_tracer=use_dynamo_tracer, enable_passes=enable_passes, + propagate_shapes=propagate_shapes, ) # Previous instance of the interpreter auto-casted 64-bit inputs diff --git a/tests/py/dynamo/conversion/test_deconvolution_aten.py b/tests/py/dynamo/conversion/test_deconvolution_aten.py index 6024b6946e..307275dba1 100644 --- a/tests/py/dynamo/conversion/test_deconvolution_aten.py +++ b/tests/py/dynamo/conversion/test_deconvolution_aten.py @@ -1,7 +1,6 @@ import torch from parameterized import param, parameterized from torch.testing._internal.common_utils import run_tests - from torch_tensorrt import Input from .harness import DispatchTestCase @@ -152,7 +151,7 @@ def forward(self, x): Input( shape=(-1, 3, -1, -1), dtype=torch.float32, - shape_ranges=[((1, 3, 1, 1), (1, 3, 4, 4), (32, 3, 128, 128))], + shape_ranges=[((1, 3, 1, 1), (2, 3, 4, 4), (32, 3, 128, 128))], ), ] self.run_test_with_dynamic_shape( @@ -221,7 +220,7 @@ def forward(self, x): Input( shape=(-1, 3, -1, -1, -1), dtype=torch.float32, - shape_ranges=[((1, 3, 1, 1, 1), (1, 3, 4, 4, 4), (8, 3, 32, 32, 32))], + shape_ranges=[((1, 3, 1, 1, 1), (2, 3, 4, 4, 4), (8, 3, 32, 32, 32))], ), ] self.run_test_with_dynamic_shape( diff --git a/tests/py/dynamo/conversion/test_embedding_bag_aten.py b/tests/py/dynamo/conversion/test_embedding_bag_aten.py index 0e4d6c806f..9664e1be58 100644 --- a/tests/py/dynamo/conversion/test_embedding_bag_aten.py +++ b/tests/py/dynamo/conversion/test_embedding_bag_aten.py @@ -144,7 +144,7 @@ def forward(self, weight, indices): inputs=[weight, indices], precision=weight.dtype, enable_passes=True, - use_dynamo_tracer=True, + propagate_shapes=True, ) @parameterized.expand( @@ -341,7 +341,7 @@ def forward(self, weight, indices, offsets): inputs=[weight, indices, offsets], precision=weight.dtype, enable_passes=True, - use_dynamo_tracer=True, + propagate_shapes=True, ) @parameterized.expand( @@ -405,7 +405,7 @@ def forward(self, weight, indices, offsets): inputs=[weight, indices, offsets], precision=weight.dtype, enable_passes=True, - use_dynamo_tracer=True, + propagate_shapes=True, ) From 2f167c6c65325e6e69e03f4367f68dff53cb6e24 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Thu, 16 May 2024 09:24:09 -0700 Subject: [PATCH 087/122] chore: updates --- tests/py/dynamo/conversion/harness.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/py/dynamo/conversion/harness.py b/tests/py/dynamo/conversion/harness.py index 3f38824c55..1c2727aa33 100644 --- a/tests/py/dynamo/conversion/harness.py +++ b/tests/py/dynamo/conversion/harness.py @@ -201,7 +201,7 @@ def generate_graph( original_inputs: List[torch.Tensor], use_dynamo_tracer: bool, enable_passes: bool, - propagate_shapes: bool, + propagate_shapes: bool = False, ): mod = mod.eval() if use_dynamo_tracer: @@ -297,6 +297,7 @@ def run_test_with_dynamic_shape( output_dtypes=None, use_dynamo_tracer=False, enable_passes=False, + propagate_shapes=False, ): mod.eval() mod = self.generate_graph( @@ -304,6 +305,7 @@ def run_test_with_dynamic_shape( input_specs, use_dynamo_tracer=use_dynamo_tracer, enable_passes=enable_passes, + propagate_shapes=propagate_shapes, ) # Previous instance of the interpreter auto-casted 64-bit inputs From 367eaf0f82250f77b5992fe6bb3c766ec5774979 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Thu, 16 May 2024 13:53:54 -0700 Subject: [PATCH 088/122] chore: updates --- py/torch_tensorrt/dynamo/_compiler.py | 2 +- py/torch_tensorrt/dynamo/conversion/aten_ops_converters.py | 2 +- .../dynamo/lowering/passes/_aten_lowering_pass.py | 7 +++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/py/torch_tensorrt/dynamo/_compiler.py b/py/torch_tensorrt/dynamo/_compiler.py index 540caad3b1..68e73e58f1 100644 --- a/py/torch_tensorrt/dynamo/_compiler.py +++ b/py/torch_tensorrt/dynamo/_compiler.py @@ -177,7 +177,7 @@ def compile( raise AssertionError( f"Input graph should be an ExportedProgram but got type {type(exported_program)}" ) - + exported_program = pre_export_lowering(exported_program, torch_inputs) exported_program = exported_program.run_decompositions( get_decompositions(enable_experimental_decompositions) ) diff --git a/py/torch_tensorrt/dynamo/conversion/aten_ops_converters.py b/py/torch_tensorrt/dynamo/conversion/aten_ops_converters.py index 0fed20a9fd..e8f10489be 100644 --- a/py/torch_tensorrt/dynamo/conversion/aten_ops_converters.py +++ b/py/torch_tensorrt/dynamo/conversion/aten_ops_converters.py @@ -571,7 +571,7 @@ def aten_ops_neg( assert torch.ops.trt.quantize_fp8.default except Exception as e: _LOGGER.warning( - f"Unable to import quantize_fp8 op.: {e}. Please install nvidia-ammo library in order to register torch.ops.trt.quantize_fp8 op" + "Unable to import torch.ops.trt.quantize_fp8 op. Please install modelopt library (https://github.com/NVIDIA/TensorRT-Model-Optimizer?tab=readme-ov-file#installation) to register torch.ops.trt.quantize_fp8 op" ) else: diff --git a/py/torch_tensorrt/dynamo/lowering/passes/_aten_lowering_pass.py b/py/torch_tensorrt/dynamo/lowering/passes/_aten_lowering_pass.py index 05a171e264..d73f234871 100644 --- a/py/torch_tensorrt/dynamo/lowering/passes/_aten_lowering_pass.py +++ b/py/torch_tensorrt/dynamo/lowering/passes/_aten_lowering_pass.py @@ -103,11 +103,10 @@ def pre_export_lowering( logging.debug( f"Invoking DynamoPassManager and applying lowering passes: {ATEN_PRE_LOWERING_PASSES}" ) - gm = ep.module() + gm = ep._graph_module gm = ATEN_PRE_LOWERING_PASSES(gm, sample_inputs) - # TODO: Check if re-exporting changes the metadata - transformed_ep = torch.export.export(gm, tuple(sample_inputs), strict=False) - return transformed_ep + ep._graph_module = gm + return ep def dump_lowering_passes() -> str: From 8cb6b91c121715bb9255ff4b914a4c6c7f217fa0 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Thu, 16 May 2024 16:25:04 -0700 Subject: [PATCH 089/122] chore: updates --- .../dynamo/lowering/passes/_aten_lowering_pass.py | 3 +-- tests/py/dynamo/models/test_models_export.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/py/torch_tensorrt/dynamo/lowering/passes/_aten_lowering_pass.py b/py/torch_tensorrt/dynamo/lowering/passes/_aten_lowering_pass.py index d73f234871..3d1663fe0b 100644 --- a/py/torch_tensorrt/dynamo/lowering/passes/_aten_lowering_pass.py +++ b/py/torch_tensorrt/dynamo/lowering/passes/_aten_lowering_pass.py @@ -103,9 +103,8 @@ def pre_export_lowering( logging.debug( f"Invoking DynamoPassManager and applying lowering passes: {ATEN_PRE_LOWERING_PASSES}" ) - gm = ep._graph_module + gm = ep.graph_module gm = ATEN_PRE_LOWERING_PASSES(gm, sample_inputs) - ep._graph_module = gm return ep diff --git a/tests/py/dynamo/models/test_models_export.py b/tests/py/dynamo/models/test_models_export.py index af9abc41f2..332872e6de 100644 --- a/tests/py/dynamo/models/test_models_export.py +++ b/tests/py/dynamo/models/test_models_export.py @@ -220,11 +220,12 @@ def calibrate_loop(model): with torch.no_grad(): with export_torch_mode(): exp_program = torch.export.export(model, (input_tensor,)) - trt_model = torch_tensorrt.dynamo.compile( + trt_model = torchtrt.dynamo.compile( exp_program, inputs=[input_tensor], enabled_precisions={torch.float8_e4m3fn}, min_block_size=1, + debug=True, ) outputs_trt = trt_model(input_tensor) assert torch.allclose(output_pyt, outputs_trt, rtol=1e-3, atol=1e-2) From 4d38368dd39c502ffaad176f05b66569f3d349ea Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Thu, 16 May 2024 16:31:52 -0700 Subject: [PATCH 090/122] chore: updates --- tests/py/dynamo/conversion/harness.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/py/dynamo/conversion/harness.py b/tests/py/dynamo/conversion/harness.py index 1c2727aa33..e93bb87743 100644 --- a/tests/py/dynamo/conversion/harness.py +++ b/tests/py/dynamo/conversion/harness.py @@ -204,8 +204,10 @@ def generate_graph( propagate_shapes: bool = False, ): mod = mod.eval() + torch_inputs = get_torch_inputs(original_inputs, _defaults.DEVICE) if use_dynamo_tracer: exported_program = torch_tensorrt.dynamo.trace(mod, tuple(original_inputs)) + exported_program = pre_export_lowering(exported_program, torch_inputs) exported_program = exported_program.run_decompositions( get_decompositions(False) ) @@ -218,7 +220,6 @@ def generate_graph( if propagate_shapes: # TODO: This is currently being used to test embedding_bag_aten due to https://github.com/pytorch/TensorRT/issues/2843 - torch_inputs = get_torch_inputs(original_inputs, _defaults.DEVICE) try: ShapeProp(fx_module).propagate(*torch_inputs) except (RuntimeError, AssertionError): From ee54da64b3098fe3f21d29b8cfbf17edca948b02 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Thu, 16 May 2024 17:16:56 -0700 Subject: [PATCH 091/122] chore: updates --- py/torch_tensorrt/dynamo/backend/backends.py | 4 ++++ py/torch_tensorrt/dynamo/lowering/__init__.py | 1 + .../dynamo/lowering/passes/remove_detach.py | 13 +++---------- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/py/torch_tensorrt/dynamo/backend/backends.py b/py/torch_tensorrt/dynamo/backend/backends.py index 6d138c4d4d..870f1da692 100644 --- a/py/torch_tensorrt/dynamo/backend/backends.py +++ b/py/torch_tensorrt/dynamo/backend/backends.py @@ -13,6 +13,7 @@ from torch_tensorrt.dynamo.lowering import ( get_decompositions, post_lowering, + remove_detach, remove_sym_nodes, repair_input_aliasing, ) @@ -82,6 +83,9 @@ def _pretraced_backend( input for input in sample_inputs if isinstance(input, torch.Tensor) ] + # Remove detach nodes + remove_detach(gm, torch_inputs) + # Invoke AOTAutograd to translate operators to aten gm = aot_export_joint_simple( gm, diff --git a/py/torch_tensorrt/dynamo/lowering/__init__.py b/py/torch_tensorrt/dynamo/lowering/__init__.py index ff2715fa5d..83c85855fe 100644 --- a/py/torch_tensorrt/dynamo/lowering/__init__.py +++ b/py/torch_tensorrt/dynamo/lowering/__init__.py @@ -6,3 +6,4 @@ from ._remove_sym_nodes import remove_sym_nodes from ._repair_input_aliasing import repair_input_aliasing from .passes import post_lowering, pre_export_lowering +from .passes.remove_detach import remove_detach diff --git a/py/torch_tensorrt/dynamo/lowering/passes/remove_detach.py b/py/torch_tensorrt/dynamo/lowering/passes/remove_detach.py index 5c47a8e288..5f1ab3738d 100644 --- a/py/torch_tensorrt/dynamo/lowering/passes/remove_detach.py +++ b/py/torch_tensorrt/dynamo/lowering/passes/remove_detach.py @@ -2,9 +2,6 @@ from typing import Sequence import torch -from torch_tensorrt.dynamo.lowering.passes.pass_utils import ( - clean_up_graph_after_modifications, -) logger = logging.getLogger(__name__) @@ -13,20 +10,16 @@ def remove_detach( gm: torch.fx.GraphModule, sample_inputs: Sequence[torch.Tensor] ) -> torch.fx.GraphModule: """Remove detach ops in the graph""" - - modified_graph = False count = 0 for node in gm.graph.nodes: - # If the node is a detach node - if node.target == torch.ops.aten.detach.default: + # node.target = "detach" in torch.compile workflow + if node.target == torch.ops.aten.detach.default or node.target == "detach": # Detach node has only one input node_input = node.all_input_nodes[0] node.replace_all_uses_with(node_input) gm.graph.erase_node(node) count += 1 - if modified_graph: - gm = clean_up_graph_after_modifications(gm) - logger.debug(f"Removed {count} detach nodes:\n{gm.graph}") + logger.debug(f"Removed {count} detach nodes:\n{gm.graph}") return gm From f4ccd62ba7aceeeb1417e5c01333724bacd90b84 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Thu, 16 May 2024 18:31:01 -0700 Subject: [PATCH 092/122] chore: updates --- py/torch_tensorrt/dynamo/conversion/aten_ops_converters.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/py/torch_tensorrt/dynamo/conversion/aten_ops_converters.py b/py/torch_tensorrt/dynamo/conversion/aten_ops_converters.py index e8f10489be..7bba7a9da6 100644 --- a/py/torch_tensorrt/dynamo/conversion/aten_ops_converters.py +++ b/py/torch_tensorrt/dynamo/conversion/aten_ops_converters.py @@ -568,6 +568,8 @@ def aten_ops_neg( try: + import modelopt.torch.quantization as mtq + assert torch.ops.trt.quantize_fp8.default except Exception as e: _LOGGER.warning( From 681a6d1aae1dbc526d213c19d078681469f65623 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Fri, 17 May 2024 09:12:08 -0700 Subject: [PATCH 093/122] chore: fixes --- tests/py/dynamo/conversion/harness.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/py/dynamo/conversion/harness.py b/tests/py/dynamo/conversion/harness.py index e93bb87743..620ec36f56 100644 --- a/tests/py/dynamo/conversion/harness.py +++ b/tests/py/dynamo/conversion/harness.py @@ -17,7 +17,11 @@ # Use interpreter, input spec, and test case from fx_ts_compat to test Dynamo Converter Registry from torch_tensorrt.dynamo.conversion import TRTInterpreter from torch_tensorrt.dynamo.conversion._conversion import infer_module_output_dtypes -from torch_tensorrt.dynamo.lowering import get_decompositions, post_lowering +from torch_tensorrt.dynamo.lowering import ( + get_decompositions, + post_lowering, + pre_export_lowering, +) from torch_tensorrt.dynamo.runtime import PythonTorchTensorRTModule from torch_tensorrt.dynamo.utils import get_torch_inputs From 44071aa1ffd2b6a49bb0656684e092ccb9f035d5 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Fri, 17 May 2024 11:38:11 -0700 Subject: [PATCH 094/122] chore: updates --- .github/workflows/build-test.yml | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 200dee7a6c..d89a842594 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -79,16 +79,15 @@ jobs: script: | export USE_HOST_DEPS=1 export LD_LIBRARY_PATH=/usr/lib64:$LD_LIBRARY_PATH - export LD_LIBRARY_PATH=/opt/torch-tensorrt-builds/TensorRT-10.0.1.6/lib:$LD_LIBRARY_PATH pushd . cd tests/modules # Don't use requirements.txt here as it contains tensorrt and torch which should have been installed by now. - ${CONDA_RUN} python -m pip install numpy packaging pyyaml transformers timm pybind11==2.6.2 + ${CONDA_RUN} python -m pip install numpy packaging pyyaml transformers==4.40.2 timm==0.9.16 pybind11==2.6.2 ${CONDA_RUN} python hub.py popd pushd . cd tests/py/ts - ${CONDA_RUN} python -m pip install --pre pytest timm transformers parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver + ${CONDA_RUN} python -m pip install --pre pytest timm==0.9.16 transformers==4.40.2 parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/ts_api_test_results.xml api/ ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/ts_models_test_results.xml models/ ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/ts_integrations_test_results.xml integrations/ @@ -117,10 +116,9 @@ jobs: pre-script: ${{ matrix.pre-script }} script: | export USE_HOST_DEPS=1 - export LD_LIBRARY_PATH=/opt/torch-tensorrt-builds/TensorRT-10.0.1.6/lib:$LD_LIBRARY_PATH pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre pytest-xdist timm transformers parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver + ${CONDA_RUN} python -m pip install --pre pytest-xdist timm==0.9.16 transformers==4.40.2 parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/dynamo_converters_test_results.xml -n 10 conversion/ popd @@ -147,10 +145,9 @@ jobs: pre-script: ${{ matrix.pre-script }} script: | export USE_HOST_DEPS=1 - export LD_LIBRARY_PATH=/opt/torch-tensorrt-builds/TensorRT-10.0.1.6/lib:$LD_LIBRARY_PATH pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre pytest timm transformers parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver + ${CONDA_RUN} python -m pip install --pre pytest timm==0.9.16 transformers==4.40.2 parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/dynamo_fe_test_results.xml --ir dynamo models/test_models_export.py ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/dyn_models_export.xml --ir dynamo models/test_dyn_models.py popd @@ -178,10 +175,9 @@ jobs: pre-script: ${{ matrix.pre-script }} script: | export USE_HOST_DEPS=1 - export LD_LIBRARY_PATH=/opt/torch-tensorrt-builds/TensorRT-10.0.1.6/lib:$LD_LIBRARY_PATH pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre pytest timm transformers parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver + ${CONDA_RUN} python -m pip install --pre pytest timm==0.9.16 transformers==4.40.2 parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/export_serde_test_results.xml --ir dynamo models/test_export_serde.py popd @@ -208,10 +204,9 @@ jobs: pre-script: ${{ matrix.pre-script }} script: | export USE_HOST_DEPS=1 - export LD_LIBRARY_PATH=/opt/torch-tensorrt-builds/TensorRT-10.0.1.6/lib:$LD_LIBRARY_PATH pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre pytest-xdist timm transformers parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver + ${CONDA_RUN} python -m pip install --pre pytest-xdist timm==0.9.16 transformers==4.40.2 parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest -n 10 --junitxml=${RUNNER_TEST_RESULTS_DIR}/torch_compile_be_test_results.xml backend/ ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/torch_comple_be_e2e_test_results.xml --ir torch_compile models/test_models.py ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/torch_compile_dyn_models_export.xml --ir torch_compile models/test_dyn_models.py @@ -240,10 +235,9 @@ jobs: pre-script: ${{ matrix.pre-script }} script: | export USE_HOST_DEPS=1 - export LD_LIBRARY_PATH=/opt/torch-tensorrt-builds/TensorRT-10.0.1.6/lib:$LD_LIBRARY_PATH pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre pytest-xdist timm transformers parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver + ${CONDA_RUN} python -m pip install --pre pytest-xdist timm==0.9.16 transformers==4.40.2 parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/tests_py_dynamo_core_runtime_test_results.xml runtime/ ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/tests_py_dynamo_core_partitioning_test_results.xml partitioning/ ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/tests_py_dynamo_core_lowering_test_results.xml lowering/ @@ -272,9 +266,8 @@ jobs: pre-script: ${{ matrix.pre-script }} script: | export USE_HOST_DEPS=1 - export LD_LIBRARY_PATH=/opt/torch-tensorrt-builds/TensorRT-10.0.1.6/lib:$LD_LIBRARY_PATH pushd . cd tests/py/core - ${CONDA_RUN} python -m pip install --pre pytest-xdist timm transformers parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver + ${CONDA_RUN} python -m pip install --pre pytest-xdist timm==0.9.16 transformers==4.40.2 parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/tests_py_core_test_results.xml . popd From 3f6999d6ab2f9b62c63b78e8405cacf216370214 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Fri, 17 May 2024 13:25:08 -0700 Subject: [PATCH 095/122] chore: updates --- .github/workflows/build-test-windows.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-test-windows.yml b/.github/workflows/build-test-windows.yml index c7f1ba1d6b..66bcc86995 100644 --- a/.github/workflows/build-test-windows.yml +++ b/.github/workflows/build-test-windows.yml @@ -72,7 +72,7 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre pytest-xdist timm transformers parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver + ${CONDA_RUN} python -m pip install --pre pytest-xdist timm==0.9.16 transformers==4.40.2 parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/dynamo_converters_test_results.xml -n 10 conversion/ popd @@ -98,7 +98,7 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre pytest-xdist timm transformers parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver + ${CONDA_RUN} python -m pip install --pre pytest-xdist timm==0.9.16 transformers==4.40.2 parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/dynamo_fe_test_results.xml --ir dynamo models/test_models_export.py ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/dyn_models_export.xml --ir dynamo models/test_dyn_models.py popd @@ -125,7 +125,7 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre pytest-xdist timm transformers parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver + ${CONDA_RUN} python -m pip install --pre pytest-xdist timm==0.9.16 transformers==4.40.2 parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest -n 10 --junitxml=${RUNNER_TEST_RESULTS_DIR}/torch_compile_be_test_results.xml backend/ ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/torch_comple_be_e2e_test_results.xml --ir torch_compile models/test_models.py popd @@ -152,7 +152,7 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre pytest-xdist timm transformers parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver + ${CONDA_RUN} python -m pip install --pre pytest-xdist timm==0.9.16 transformers==4.40.2 parameterized expecttest==0.1.6 --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/tests_py_dynamo_core_runtime_test_results.xml runtime/ ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/tests_py_dynamo_core_partitioning_test_results.xml partitioning/ ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/tests_py_dynamo_core_lowering_test_results.xml lowering/ From 5de9325b917ec2d49701919520f9d43b2cede576 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Fri, 17 May 2024 14:40:14 -0700 Subject: [PATCH 096/122] chore: updates --- py/torch_tensorrt/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/py/torch_tensorrt/__init__.py b/py/torch_tensorrt/__init__.py index 1b9faf08a8..156ec843dd 100644 --- a/py/torch_tensorrt/__init__.py +++ b/py/torch_tensorrt/__init__.py @@ -6,7 +6,6 @@ from torch_tensorrt._version import ( # noqa: F401 __cuda_version__, - __cudnn_version__, __tensorrt_version__, __version__, ) @@ -40,11 +39,9 @@ def _find_lib(name: str, paths: List[str]) -> str: import tensorrt # noqa: F401 except ImportError: cuda_version = _parse_semver(__cuda_version__) - cudnn_version = _parse_semver(__cudnn_version__) tensorrt_version = _parse_semver(__tensorrt_version__) CUDA_MAJOR = cuda_version["major"] - CUDNN_MAJOR = cudnn_version["major"] TENSORRT_MAJOR = tensorrt_version["major"] if sys.platform.startswith("win"): From c677ef9612d40c1ada52ee6011c029e5dd9f49ef Mon Sep 17 00:00:00 2001 From: Evan Li Date: Tue, 21 May 2024 16:20:16 -0700 Subject: [PATCH 097/122] refactor vgg16 with fp8 and ptq example --- docsrc/index.rst | 1 + examples/dynamo/README.rst | 1 + examples/dynamo/vgg16_fp8_ptq.py | 261 ++++++++++++++++++ examples/int8/training/vgg16/requirements.txt | 2 + 4 files changed, 265 insertions(+) create mode 100644 examples/dynamo/vgg16_fp8_ptq.py diff --git a/docsrc/index.rst b/docsrc/index.rst index 455aeab8b3..2842e3f9cd 100644 --- a/docsrc/index.rst +++ b/docsrc/index.rst @@ -111,6 +111,7 @@ Tutorials tutorials/_rendered_examples/dynamo/torch_compile_transformers_example tutorials/_rendered_examples/dynamo/torch_compile_advanced_usage tutorials/_rendered_examples/dynamo/torch_compile_stable_diffusion + tutorials/_rendered_examples/dynamo/vgg16_fp8_ptq Python API Documenation ------------------------ diff --git a/examples/dynamo/README.rst b/examples/dynamo/README.rst index d895cc0113..cf8cd831c0 100644 --- a/examples/dynamo/README.rst +++ b/examples/dynamo/README.rst @@ -10,3 +10,4 @@ a number of ways you can leverage this backend to accelerate inference. * :ref:`torch_compile_transformer`: Compiling a Transformer model using ``torch.compile`` * :ref:`torch_compile_advanced_usage`: Advanced usage including making a custom backend to use directly with the ``torch.compile`` API * :ref:`torch_compile_stable_diffusion`: Compiling a Stable Diffusion model using ``torch.compile`` +* :ref:`vgg16_fp8_ptq`: Compiling a VGG16 model with FP8 and PTQ using ``torch.compile`` diff --git a/examples/dynamo/vgg16_fp8_ptq.py b/examples/dynamo/vgg16_fp8_ptq.py new file mode 100644 index 0000000000..5432dea7d6 --- /dev/null +++ b/examples/dynamo/vgg16_fp8_ptq.py @@ -0,0 +1,261 @@ +""" +.. _vgg16_fp8_ptq: + +Torch Compile VGG16 with FP8 and PTQ +====================================================== + +This script is intended as a sample of the Torch-TensorRT workflow with `torch.compile` on a VGG16 model with FP8 and PTQ. +""" + +# %% +# Imports and Model Definition +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +import argparse + +import modelopt.torch.quantization as mtq +import torch +import torch.nn as nn +import torch.nn.functional as F +import torch_tensorrt as torchtrt +import torchvision.datasets as datasets +import torchvision.transforms as transforms +from modelopt.torch.quantization.utils import export_torch_mode + + +class VGG(nn.Module): + def __init__(self, layer_spec, num_classes=1000, init_weights=False): + super(VGG, self).__init__() + + layers = [] + in_channels = 3 + for l in layer_spec: + if l == "pool": + layers.append(nn.MaxPool2d(kernel_size=2, stride=2)) + else: + layers += [ + nn.Conv2d(in_channels, l, kernel_size=3, padding=1), + nn.BatchNorm2d(l), + nn.ReLU(), + ] + in_channels = l + + self.features = nn.Sequential(*layers) + self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) + self.classifier = nn.Sequential( + nn.Linear(512 * 1 * 1, 4096), + nn.ReLU(), + nn.Dropout(), + nn.Linear(4096, 4096), + nn.ReLU(), + nn.Dropout(), + nn.Linear(4096, num_classes), + ) + if init_weights: + self._initialize_weights() + + def _initialize_weights(self): + for m in self.modules(): + if isinstance(m, nn.Conv2d): + nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu") + if m.bias is not None: + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.BatchNorm2d): + nn.init.constant_(m.weight, 1) + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.Linear): + nn.init.normal_(m.weight, 0, 0.01) + nn.init.constant_(m.bias, 0) + + def forward(self, x): + x = self.features(x) + x = self.avgpool(x) + x = torch.flatten(x, 1) + x = self.classifier(x) + return x + + +def vgg16(num_classes=1000, init_weights=False): + vgg16_cfg = [ + 64, + 64, + "pool", + 128, + 128, + "pool", + 256, + 256, + 256, + "pool", + 512, + 512, + 512, + "pool", + 512, + 512, + 512, + "pool", + ] + return VGG(vgg16_cfg, num_classes, init_weights) + + +PARSER = argparse.ArgumentParser( + description="Load pre-trained VGG model and then tune with FP8 and PTQ" +) +PARSER.add_argument("--ckpt", type=str, help="Path to the pre-trained checkpoint") +PARSER.add_argument( + "--batch-size", + default=128, + type=int, + help="Batch size for tuning the model with PTQ", +) +PARSER.add_argument( + "--fp8-epochs", + default=10, + type=int, + help="The number of epochs to quantize the model to FP8", +) + +args = PARSER.parse_args() + +model = vgg16(num_classes=10, init_weights=False) +model = model.cuda() + +# %% +# Load the pre-trained model weights +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +ckpt = torch.load(args.ckpt) +weights = ckpt["model_state_dict"] + +if torch.cuda.device_count() > 1: + from collections import OrderedDict + + new_state_dict = OrderedDict() + for k, v in weights.items(): + name = k[7:] # remove `module.` + new_state_dict[name] = v + weights = new_state_dict + +model.load_state_dict(weights) +# Don't forget to set the model to evaluation mode! +model.eval() + +# %% +# Load training dataset and define loss function for PTQ +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +training_dataset = datasets.CIFAR10( + root="./data", + train=True, + download=True, + transform=transforms.Compose( + [ + transforms.RandomCrop(32, padding=4), + transforms.RandomHorizontalFlip(), + transforms.ToTensor(), + transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)), + ] + ), +) +training_dataloader = torch.utils.data.DataLoader( + training_dataset, batch_size=args.batch_size, shuffle=True, num_workers=2 +) + +data = iter(training_dataloader) +images, _ = next(data) + +crit = nn.CrossEntropyLoss() + +# %% +# Define Calibration Loop for quantization +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + +def calibrate_loop(model): + # calibrate on a small number of batches + for fp8_ep in range(args.fp8_epochs): + print("Epoch: [%5d / %5d]" % (fp8_ep + 1, args.fp8_epochs)) + total = 0 + correct = 0 + loss = 0.0 + for data, labels in training_dataloader: + data, labels = data.cuda(), labels.cuda(non_blocking=True) + out = model(data) + loss += crit(out, labels) + preds = torch.max(out, 1)[1] + total += labels.size(0) + correct += (preds == labels).sum().item() + + print( + "PTQ Training Loss: {:.5f} Acc: {:.2f}%".format( + loss / total, 100 * correct / total + ) + ) + + +# %% +# Tune the pre-trained model with FP8 and PTQ +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +quant_cfg = mtq.FP8_DEFAULT_CFG +# PTQ with in-place replacement to quantized modules +mtq.quantize(model, quant_cfg, forward_loop=calibrate_loop) +# model has FP8 qdq nodes at this point + +# %% +# Inference +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +# Load the testing dataset +testing_dataset = datasets.CIFAR10( + root="./data", + train=False, + download=True, + transform=transforms.Compose( + [ + transforms.ToTensor(), + transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)), + ] + ), +) + +testing_dataloader = torch.utils.data.DataLoader( + testing_dataset, batch_size=args.batch_size, shuffle=False, num_workers=2 +) + +with torch.no_grad(): + with export_torch_mode(): + # Compile the model with Torch-TensorRT Dynamo backend + input_tensor = images.cuda() + exp_program = torch.export.export(model, (input_tensor,)) + trt_model = torchtrt.dynamo.compile( + exp_program, + inputs=[input_tensor], + enabled_precisions={torch.float8_e4m3fn}, + min_block_size=1, + debug=False, + ) + + # Inference compiled Torch-TensorRT model over the testing dataset + total = 0 + correct = 0 + loss = 0.0 + class_probs = [] + class_preds = [] + model.eval() + for data, labels in testing_dataloader: + data, labels = data.cuda(), labels.cuda(non_blocking=True) + out = model(data) + loss += crit(out, labels) + preds = torch.max(out, 1)[1] + class_probs.append([F.softmax(i, dim=0) for i in out]) + class_preds.append(preds) + total += labels.size(0) + correct += (preds == labels).sum().item() + + test_probs = torch.cat([torch.stack(batch) for batch in class_probs]) + test_preds = torch.cat(class_preds) + test_loss = loss / total + test_acc = correct / total + print("Test Loss: {:.5f} Test Acc: {:.2f}%".format(test_loss, 100 * test_acc)) diff --git a/examples/int8/training/vgg16/requirements.txt b/examples/int8/training/vgg16/requirements.txt index d02af2c616..3b0b03f5d7 100644 --- a/examples/int8/training/vgg16/requirements.txt +++ b/examples/int8/training/vgg16/requirements.txt @@ -4,3 +4,5 @@ nvidia-pyindex --extra-index-url https://pypi.nvidia.com pytorch-quantization tqdm +nvidia-modelopt +--extra-index-url https://pypi.nvidia.com From f0b8d47daf6a7f1e34a4f074b02c6b127601ed46 Mon Sep 17 00:00:00 2001 From: Evan Li Date: Wed, 22 May 2024 15:27:23 -0700 Subject: [PATCH 098/122] fix bugs --- examples/dynamo/vgg16_fp8_ptq.py | 44 ++++++++++++-------------------- 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/examples/dynamo/vgg16_fp8_ptq.py b/examples/dynamo/vgg16_fp8_ptq.py index 5432dea7d6..b2a82cc4f8 100644 --- a/examples/dynamo/vgg16_fp8_ptq.py +++ b/examples/dynamo/vgg16_fp8_ptq.py @@ -102,18 +102,14 @@ def vgg16(num_classes=1000, init_weights=False): PARSER = argparse.ArgumentParser( description="Load pre-trained VGG model and then tune with FP8 and PTQ" ) -PARSER.add_argument("--ckpt", type=str, help="Path to the pre-trained checkpoint") PARSER.add_argument( - "--batch-size", - default=128, - type=int, - help="Batch size for tuning the model with PTQ", + "--ckpt", type=str, required=True, help="Path to the pre-trained checkpoint" ) PARSER.add_argument( - "--fp8-epochs", - default=10, + "--batch-size", + default=128, type=int, - help="The number of epochs to quantize the model to FP8", + help="Batch size for tuning the model with PTQ and FP8", ) args = PARSER.parse_args() @@ -173,25 +169,19 @@ def vgg16(num_classes=1000, init_weights=False): def calibrate_loop(model): - # calibrate on a small number of batches - for fp8_ep in range(args.fp8_epochs): - print("Epoch: [%5d / %5d]" % (fp8_ep + 1, args.fp8_epochs)) - total = 0 - correct = 0 - loss = 0.0 - for data, labels in training_dataloader: - data, labels = data.cuda(), labels.cuda(non_blocking=True) - out = model(data) - loss += crit(out, labels) - preds = torch.max(out, 1)[1] - total += labels.size(0) - correct += (preds == labels).sum().item() - - print( - "PTQ Training Loss: {:.5f} Acc: {:.2f}%".format( - loss / total, 100 * correct / total - ) - ) + # calibrate over the training dataset + total = 0 + correct = 0 + loss = 0.0 + for data, labels in training_dataloader: + data, labels = data.cuda(), labels.cuda(non_blocking=True) + out = model(data) + loss += crit(out, labels) + preds = torch.max(out, 1)[1] + total += labels.size(0) + correct += (preds == labels).sum().item() + + print("PTQ Loss: {:.5f} Acc: {:.2f}%".format(loss / total, 100 * correct / total)) # %% From beb888dec855b029bbb2ae1905e2efe60c1323b1 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Thu, 23 May 2024 10:00:02 -0700 Subject: [PATCH 099/122] chore: updates --- py/torch_tensorrt/dynamo/conversion/impl/quantize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/torch_tensorrt/dynamo/conversion/impl/quantize.py b/py/torch_tensorrt/dynamo/conversion/impl/quantize.py index 478e8a5771..ac9252a182 100644 --- a/py/torch_tensorrt/dynamo/conversion/impl/quantize.py +++ b/py/torch_tensorrt/dynamo/conversion/impl/quantize.py @@ -23,7 +23,7 @@ def quantize_fp8( on the output_type set and dequantizes them back. """ if (isinstance(input_tensor, TRTTensor)) and not ( - input_tensor.dtype != trt.float32 or input_tensor.dtype != trt.float16 + input_tensor.dtype == trt.float32 or input_tensor.dtype == trt.float16 ): raise ValueError( f"quantize_fp8 converter received an input of {input_tensor.dtype} type. Supported types: float32 | float16" From e7989a0185e30d10805e85dd259dbc7eba018a65 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Thu, 23 May 2024 12:14:04 -0700 Subject: [PATCH 100/122] chore: address review comments --- py/torch_tensorrt/dynamo/conversion/aten_ops_converters.py | 2 +- py/torch_tensorrt/dynamo/conversion/impl/quantize.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/py/torch_tensorrt/dynamo/conversion/aten_ops_converters.py b/py/torch_tensorrt/dynamo/conversion/aten_ops_converters.py index 4c17902882..9113d6ed22 100644 --- a/py/torch_tensorrt/dynamo/conversion/aten_ops_converters.py +++ b/py/torch_tensorrt/dynamo/conversion/aten_ops_converters.py @@ -585,7 +585,7 @@ def aten_ops_neg( assert torch.ops.trt.quantize_fp8.default except Exception as e: _LOGGER.warning( - "Unable to import torch.ops.trt.quantize_fp8 op. Please install modelopt library (https://github.com/NVIDIA/TensorRT-Model-Optimizer?tab=readme-ov-file#installation) to register torch.ops.trt.quantize_fp8 op" + "Unable to import quantization op. Please install modelopt library (https://github.com/NVIDIA/TensorRT-Model-Optimizer?tab=readme-ov-file#installation) to add support for compiling quantized models" ) else: diff --git a/py/torch_tensorrt/dynamo/conversion/impl/quantize.py b/py/torch_tensorrt/dynamo/conversion/impl/quantize.py index ac9252a182..de78385ce9 100644 --- a/py/torch_tensorrt/dynamo/conversion/impl/quantize.py +++ b/py/torch_tensorrt/dynamo/conversion/impl/quantize.py @@ -29,8 +29,7 @@ def quantize_fp8( f"quantize_fp8 converter received an input of {input_tensor.dtype} type. Supported types: float32 | float16" ) - if isinstance(scale, np.ndarray): - scale = get_trt_tensor(ctx, scale, name + "_scale") + scale = get_trt_tensor(ctx, scale, name + "_scale") # Add Q node quantize_layer = ctx.net.add_quantize(input_tensor, scale) quantize_layer.set_output_type(0, trt.DataType.FP8) From ad9d82507bb7d3bf0a708b9a83fab3ce77184b45 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Fri, 24 May 2024 11:08:41 -0700 Subject: [PATCH 101/122] chore: updates --- tests/py/dynamo/lowering/test_aten_lowering_passes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/py/dynamo/lowering/test_aten_lowering_passes.py b/tests/py/dynamo/lowering/test_aten_lowering_passes.py index 0dd9a8de1c..d756ac8da2 100644 --- a/tests/py/dynamo/lowering/test_aten_lowering_passes.py +++ b/tests/py/dynamo/lowering/test_aten_lowering_passes.py @@ -2,9 +2,8 @@ import unittest import torch -from torch.testing._internal.common_utils import TestCase, run_tests - import torch_tensorrt +from torch.testing._internal.common_utils import TestCase, run_tests from ..testing_utilities import DECIMALS_OF_AGREEMENT, lower_graph_testing @@ -397,6 +396,9 @@ def forward(self, q, k, v): class TestLowerLinear(TestCase): + @unittest.skip( + "This test has threshold failures. This is tracked at https://github.com/pytorch/TensorRT/issues/2715", + ) def test_lower_linear(self): class Linear(torch.nn.Module): def forward(self, input, weight, bias): From 0059c1c8888756c2fde47c8320dd5955c84cdfe9 Mon Sep 17 00:00:00 2001 From: Naren Dasan <1790613+narendasan@users.noreply.github.com> Date: Fri, 24 May 2024 12:41:19 -0600 Subject: [PATCH 102/122] Update build-test-windows.yml --- .github/workflows/build-test-windows.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-test-windows.yml b/.github/workflows/build-test-windows.yml index 03c658ae6b..f39286167c 100644 --- a/.github/workflows/build-test-windows.yml +++ b/.github/workflows/build-test-windows.yml @@ -72,7 +72,7 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt + ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/dynamo_converters_test_results.xml -n 10 conversion/ popd @@ -98,7 +98,7 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt + ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/dynamo_fe_test_results.xml --ir dynamo models/test_models_export.py ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/dyn_models_export.xml --ir dynamo models/test_dyn_models.py popd @@ -125,7 +125,7 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt + ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest -n 10 --junitxml=${RUNNER_TEST_RESULTS_DIR}/torch_compile_be_test_results.xml backend/ ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/torch_comple_be_e2e_test_results.xml --ir torch_compile models/test_models.py popd @@ -152,7 +152,7 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt + ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/tests_py_dynamo_core_runtime_test_results.xml runtime/ ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/tests_py_dynamo_core_partitioning_test_results.xml partitioning/ ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/tests_py_dynamo_core_lowering_test_results.xml lowering/ From f98abd686f8088397493ff127d6a708edc642f1e Mon Sep 17 00:00:00 2001 From: Naren Dasan <1790613+narendasan@users.noreply.github.com> Date: Fri, 24 May 2024 12:40:58 -0600 Subject: [PATCH 103/122] Update build-test-linux.yml --- .github/workflows/build-test-linux.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-test-linux.yml b/.github/workflows/build-test-linux.yml index 1bad21deef..5de2d272ec 100644 --- a/.github/workflows/build-test-linux.yml +++ b/.github/workflows/build-test-linux.yml @@ -81,7 +81,7 @@ jobs: export LD_LIBRARY_PATH=/usr/lib64:$LD_LIBRARY_PATH pushd . cd tests/modules - ${CONDA_RUN} python -m pip install --pre -r ../py/requirements.txt + ${CONDA_RUN} python -m pip install --pre -r ../py/requirements.txt --use-deprecated=legacy-resolver ${CONDA_RUN} python hub.py popd pushd . @@ -116,7 +116,7 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt + ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/dynamo_converters_test_results.xml -n 10 conversion/ popd @@ -145,7 +145,7 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt + ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/dynamo_fe_test_results.xml --ir dynamo models/test_models_export.py ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/dyn_models_export.xml --ir dynamo models/test_dyn_models.py popd @@ -175,7 +175,7 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt + ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/export_serde_test_results.xml --ir dynamo models/test_export_serde.py popd @@ -204,7 +204,7 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt + ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest -n 10 --junitxml=${RUNNER_TEST_RESULTS_DIR}/torch_compile_be_test_results.xml backend/ ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/torch_comple_be_e2e_test_results.xml --ir torch_compile models/test_models.py ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/torch_compile_dyn_models_export.xml --ir torch_compile models/test_dyn_models.py @@ -235,7 +235,7 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt + ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/tests_py_dynamo_core_runtime_test_results.xml runtime/ ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/tests_py_dynamo_core_partitioning_test_results.xml partitioning/ ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/tests_py_dynamo_core_lowering_test_results.xml lowering/ @@ -266,6 +266,6 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/core - ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt + ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/tests_py_core_test_results.xml . popd From 0d2021dcd4278e65f2860f23a78015eb60b3e2c1 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Fri, 24 May 2024 13:58:05 -0700 Subject: [PATCH 104/122] chore: updates --- .github/scripts/install-torch-tensorrt.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/install-torch-tensorrt.sh b/.github/scripts/install-torch-tensorrt.sh index 32d6613c10..fa4a72ae5f 100644 --- a/.github/scripts/install-torch-tensorrt.sh +++ b/.github/scripts/install-torch-tensorrt.sh @@ -2,7 +2,7 @@ set -eou pipefail # Source conda so it's available to the script environment source ${BUILD_ENV_FILE} -${CONDA_RUN} ${PIP_INSTALL_TORCH} torchvision +${CONDA_RUN} ${PIP_INSTALL_TORCH} torchvision==0.18.0 ${CONDA_RUN} python -m pip install pyyaml mpmath==1.3.0 export TRT_VERSION=$(${CONDA_RUN} python -c "import versions; versions.tensorrt_version()") From 1940267a28eb96136f112313822ad453009513a7 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 27 May 2024 01:05:00 -0700 Subject: [PATCH 105/122] chore: updates --- .../scripts/install-torch-tensorrt-windows.sh | 15 +++++++++++--- .github/scripts/install-torch-tensorrt.sh | 20 ++++++++++++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/.github/scripts/install-torch-tensorrt-windows.sh b/.github/scripts/install-torch-tensorrt-windows.sh index 81adc4e1d5..c8c5e45dc4 100644 --- a/.github/scripts/install-torch-tensorrt-windows.sh +++ b/.github/scripts/install-torch-tensorrt-windows.sh @@ -1,12 +1,21 @@ set -eou pipefail source "${BUILD_ENV_FILE}" -# Install test index version of Torch and Torchvision -${CONDA_RUN} ${PIP_INSTALL_TORCH} torchvision +# Install stable versions of Torch and Torchvision since this is a release branch. +# For main branches, the INDEX_URL will always point to nightly and for RC releases, +# it will contain test tag in the url. +export TORCH_VERSION_CI="2.3.0" +export TORCHVISION_VERSION_CI="0.18.0" +export TENSORRT_VERSION_CI="10.0.1" +export INDEX_URL="https://download.pytorch.org/whl/${CU_VERSION}" + ${CONDA_RUN} pip install pyyaml mpmath==1.3.0 +# Install appropriate torch and torchvision versions for the platform +${CONDA_RUN} pip install torch==${TORCH_VERSION_CI} torchvision==${TORCHVISION_VERSION_CI} --index-url ${INDEX_URL} + # Install TRT 10 from PyPi -${CONDA_RUN} pip install tensorrt==10.0.0b6 tensorrt-${CU_VERSION::4}-bindings==10.0.0b6 tensorrt-${CU_VERSION::4}-libs==10.0.0b6 --extra-index-url https://pypi.nvidia.com +${CONDA_RUN} pip install tensorrt==${TENSORRT_VERSION_CI} # Install pre-built Torch-TRT ${CONDA_RUN} pip install ${RUNNER_ARTIFACT_DIR}/torch_tensorrt*.whl diff --git a/.github/scripts/install-torch-tensorrt.sh b/.github/scripts/install-torch-tensorrt.sh index fa4a72ae5f..822b3bcf0b 100644 --- a/.github/scripts/install-torch-tensorrt.sh +++ b/.github/scripts/install-torch-tensorrt.sh @@ -2,11 +2,25 @@ set -eou pipefail # Source conda so it's available to the script environment source ${BUILD_ENV_FILE} -${CONDA_RUN} ${PIP_INSTALL_TORCH} torchvision==0.18.0 + +# Install stable versions of Torch and Torchvision since this is a release branch. +# For main branches, the INDEX_URL will always point to nightly and for RC releases, +# it will contain test tag in the url. +export TORCH_VERSION_CI="2.3.0" +export TORCHVISION_VERSION_CI="0.18.0" +export TENSORRT_VERSION_CI="10.0.1" +export INDEX_URL="https://download.pytorch.org/whl/${CU_VERSION}" + + ${CONDA_RUN} python -m pip install pyyaml mpmath==1.3.0 -export TRT_VERSION=$(${CONDA_RUN} python -c "import versions; versions.tensorrt_version()") + +# Install appropriate torch and torchvision versions for the platform +${CONDA_RUN} pip install torch==${TORCH_VERSION_CI} torchvision==${TORCHVISION_VERSION_CI} --index-url ${INDEX_URL} + +# Install TRT 10 from PyPi +${CONDA_RUN} pip install tensorrt==${TENSORRT_VERSION_CI} # Install Torch-TensorRT -${CONDA_RUN} python -m pip install /opt/torch-tensorrt-builds/torch_tensorrt*+${CU_VERSION}*.whl tensorrt~=${TRT_VERSION} --extra-index-url=https://pypi.ngc.nvidia.com +${CONDA_RUN} python -m pip install /opt/torch-tensorrt-builds/torch_tensorrt*+${CU_VERSION}*.whl echo -e "Running test script"; From 58144024f9392d8541792c6d89ae17ffd95be62c Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 27 May 2024 11:37:14 -0700 Subject: [PATCH 106/122] chore: disable all lower_linear tests --- tests/py/dynamo/lowering/test_aten_lowering_passes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/py/dynamo/lowering/test_aten_lowering_passes.py b/tests/py/dynamo/lowering/test_aten_lowering_passes.py index d756ac8da2..840a556e44 100644 --- a/tests/py/dynamo/lowering/test_aten_lowering_passes.py +++ b/tests/py/dynamo/lowering/test_aten_lowering_passes.py @@ -466,6 +466,9 @@ def forward(self, input, weight, bias): ) torch._dynamo.reset() + @unittest.skip( + "This test has threshold failures. This is tracked at https://github.com/pytorch/TensorRT/issues/2715", + ) def test_lower_linear_batch(self): class Linear(torch.nn.Module): def forward(self, input, weight, bias): From 338a92b952095c237571a80ab4fdb2da79b5fc93 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 27 May 2024 13:13:34 -0700 Subject: [PATCH 107/122] chore: updates --- .../scripts/install-torch-tensorrt-windows.sh | 17 ++++++------ .github/scripts/install-torch-tensorrt.sh | 19 +++++++------- versions.py | 26 ++++++++++++++++++- 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/.github/scripts/install-torch-tensorrt-windows.sh b/.github/scripts/install-torch-tensorrt-windows.sh index c8c5e45dc4..00cc5aa32d 100644 --- a/.github/scripts/install-torch-tensorrt-windows.sh +++ b/.github/scripts/install-torch-tensorrt-windows.sh @@ -1,18 +1,19 @@ set -eou pipefail source "${BUILD_ENV_FILE}" +# Install pyyaml first to parse dev_dep_versions.yml in versions.py +${CONDA_RUN} pip install pyyaml + # Install stable versions of Torch and Torchvision since this is a release branch. # For main branches, the INDEX_URL will always point to nightly and for RC releases, # it will contain test tag in the url. -export TORCH_VERSION_CI="2.3.0" -export TORCHVISION_VERSION_CI="0.18.0" -export TENSORRT_VERSION_CI="10.0.1" -export INDEX_URL="https://download.pytorch.org/whl/${CU_VERSION}" - -${CONDA_RUN} pip install pyyaml mpmath==1.3.0 +export TORCH_VERSION_CI=$(${CONDA_RUN} python -c "import versions; versions.torch_version()") +export TORCHVISION_VERSION_CI=$(${CONDA_RUN} python -c "import versions; versions.torchvision_version()") +export TENSORRT_VERSION_CI=$(${CONDA_RUN} python -c "import versions; versions.tensorrt_version()") +export INDEX_URL_CI=$(${CONDA_RUN} python -c "import versions; versions.index_url()") # Install appropriate torch and torchvision versions for the platform -${CONDA_RUN} pip install torch==${TORCH_VERSION_CI} torchvision==${TORCHVISION_VERSION_CI} --index-url ${INDEX_URL} +${CONDA_RUN} pip install torch==${TORCH_VERSION_CI} torchvision==${TORCHVISION_VERSION_CI} --index-url ${INDEX_URL_CI} # Install TRT 10 from PyPi ${CONDA_RUN} pip install tensorrt==${TENSORRT_VERSION_CI} @@ -20,4 +21,4 @@ ${CONDA_RUN} pip install tensorrt==${TENSORRT_VERSION_CI} # Install pre-built Torch-TRT ${CONDA_RUN} pip install ${RUNNER_ARTIFACT_DIR}/torch_tensorrt*.whl -echo -e "Running test script"; +echo -e "Running test script"; \ No newline at end of file diff --git a/.github/scripts/install-torch-tensorrt.sh b/.github/scripts/install-torch-tensorrt.sh index 822b3bcf0b..367fb14ddd 100644 --- a/.github/scripts/install-torch-tensorrt.sh +++ b/.github/scripts/install-torch-tensorrt.sh @@ -3,24 +3,23 @@ set -eou pipefail # Source conda so it's available to the script environment source ${BUILD_ENV_FILE} +# Install pyyaml first to parse dev_dep_versions.yml in versions.py +${CONDA_RUN} python -m pip install pyyaml + # Install stable versions of Torch and Torchvision since this is a release branch. # For main branches, the INDEX_URL will always point to nightly and for RC releases, # it will contain test tag in the url. -export TORCH_VERSION_CI="2.3.0" -export TORCHVISION_VERSION_CI="0.18.0" -export TENSORRT_VERSION_CI="10.0.1" -export INDEX_URL="https://download.pytorch.org/whl/${CU_VERSION}" - - -${CONDA_RUN} python -m pip install pyyaml mpmath==1.3.0 +export TORCH_VERSION_CI=$(${CONDA_RUN} python -c "import versions; versions.torch_version()") +export TORCHVISION_VERSION_CI=$(${CONDA_RUN} python -c "import versions; versions.torchvision_version()") +export TENSORRT_VERSION_CI=$(${CONDA_RUN} python -c "import versions; versions.tensorrt_version()") +export INDEX_URL_CI=$(${CONDA_RUN} python -c "import versions; versions.index_url()") # Install appropriate torch and torchvision versions for the platform -${CONDA_RUN} pip install torch==${TORCH_VERSION_CI} torchvision==${TORCHVISION_VERSION_CI} --index-url ${INDEX_URL} +${CONDA_RUN} pip install torch==${TORCH_VERSION_CI} torchvision==${TORCHVISION_VERSION_CI} --index-url ${INDEX_URL_CI} # Install TRT 10 from PyPi ${CONDA_RUN} pip install tensorrt==${TENSORRT_VERSION_CI} # Install Torch-TensorRT ${CONDA_RUN} python -m pip install /opt/torch-tensorrt-builds/torch_tensorrt*+${CU_VERSION}*.whl - -echo -e "Running test script"; +echo -e "Running test script"; \ No newline at end of file diff --git a/versions.py b/versions.py index db418a06d2..cf9b3603bd 100644 --- a/versions.py +++ b/versions.py @@ -10,7 +10,12 @@ __cuda_version__ = "0.0" __cudnn_version__ = "0.0" __tensorrt_version__ = "0.0" - +# The following variables are used to install the correct versions of torch & torchvision in CI +__torch_version__ = "0.0" +__torchvision_version__ = "0.0" +# / is necessary at the end +__index_url__ = "https://download.pytorch.org/whl/test/" +DEFAULT_CUDA_INDEX = "cu121" LEADING_V_PATTERN = re.compile("^v") TRAILING_RC_PATTERN = re.compile("-rc[0-9]*$") @@ -106,6 +111,9 @@ def load_dep_info(): global __cuda_version__ global __cudnn_version__ global __tensorrt_version__ + global __torch_version__ + global __torchvision_version__ + global __index_url__ with open("dev_dep_versions.yml", "r") as stream: versions = yaml.safe_load(stream) gpu_arch_version = os.environ.get("CU_VERSION") @@ -113,10 +121,14 @@ def load_dep_info(): __cuda_version__ = ( (gpu_arch_version[2:])[:-1] + "." + (gpu_arch_version[2:])[-1:] ) + __index_url__ = versions["__index_url__"] + gpu_arch_version else: __cuda_version__ = versions["__cuda_version__"] + __index_url__ = versions["__index_url__"] + DEFAULT_CUDA_INDEX __cudnn_version__ = versions["__cudnn_version__"] __tensorrt_version__ = versions["__tensorrt_version__"] + __torch_version__ = versions["__torch_version__"] + __torchvision_version__ = versions["__torchvision_version__"] load_dep_info() @@ -147,3 +159,15 @@ def cudnn_version(): def tensorrt_version(): print(__tensorrt_version__) + + +def torch_version(): + print(__torch_version__) + + +def torchvision_version(): + print(__torchvision_version__) + + +def index_url(): + print(__index_url__) From 59d0bd0ef5b434d1f24f3f792882b7cddd6883b5 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 27 May 2024 13:53:50 -0700 Subject: [PATCH 108/122] chore: fixes --- dev_dep_versions.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dev_dep_versions.yml b/dev_dep_versions.yml index 2838da15c5..3f11248ce1 100644 --- a/dev_dep_versions.yml +++ b/dev_dep_versions.yml @@ -2,3 +2,7 @@ __version__: "2.3.0" __cuda_version__: "12.1" __cudnn_version__: "8.9" __tensorrt_version__: "10.0.1" +__torch_version__: "2.3.0" +# torchvision version here is not a direct dependency but the one used during testing +__torchvision_version__: "0.18.0" +__index_url__: "https://download.pytorch.org/whl/test/" \ No newline at end of file From 020fe63916c7039781445b1f352db611576e8da1 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 27 May 2024 15:19:50 -0700 Subject: [PATCH 109/122] chore: updates --- .../scripts/install-torch-tensorrt-windows.sh | 24 ------------------- .github/scripts/install-torch-tensorrt.sh | 6 ++++- 2 files changed, 5 insertions(+), 25 deletions(-) delete mode 100644 .github/scripts/install-torch-tensorrt-windows.sh diff --git a/.github/scripts/install-torch-tensorrt-windows.sh b/.github/scripts/install-torch-tensorrt-windows.sh deleted file mode 100644 index 00cc5aa32d..0000000000 --- a/.github/scripts/install-torch-tensorrt-windows.sh +++ /dev/null @@ -1,24 +0,0 @@ -set -eou pipefail -source "${BUILD_ENV_FILE}" - -# Install pyyaml first to parse dev_dep_versions.yml in versions.py -${CONDA_RUN} pip install pyyaml - -# Install stable versions of Torch and Torchvision since this is a release branch. -# For main branches, the INDEX_URL will always point to nightly and for RC releases, -# it will contain test tag in the url. -export TORCH_VERSION_CI=$(${CONDA_RUN} python -c "import versions; versions.torch_version()") -export TORCHVISION_VERSION_CI=$(${CONDA_RUN} python -c "import versions; versions.torchvision_version()") -export TENSORRT_VERSION_CI=$(${CONDA_RUN} python -c "import versions; versions.tensorrt_version()") -export INDEX_URL_CI=$(${CONDA_RUN} python -c "import versions; versions.index_url()") - -# Install appropriate torch and torchvision versions for the platform -${CONDA_RUN} pip install torch==${TORCH_VERSION_CI} torchvision==${TORCHVISION_VERSION_CI} --index-url ${INDEX_URL_CI} - -# Install TRT 10 from PyPi -${CONDA_RUN} pip install tensorrt==${TENSORRT_VERSION_CI} - -# Install pre-built Torch-TRT -${CONDA_RUN} pip install ${RUNNER_ARTIFACT_DIR}/torch_tensorrt*.whl - -echo -e "Running test script"; \ No newline at end of file diff --git a/.github/scripts/install-torch-tensorrt.sh b/.github/scripts/install-torch-tensorrt.sh index 367fb14ddd..5f33addb74 100644 --- a/.github/scripts/install-torch-tensorrt.sh +++ b/.github/scripts/install-torch-tensorrt.sh @@ -21,5 +21,9 @@ ${CONDA_RUN} pip install torch==${TORCH_VERSION_CI} torchvision==${TORCHVISION_V ${CONDA_RUN} pip install tensorrt==${TENSORRT_VERSION_CI} # Install Torch-TensorRT -${CONDA_RUN} python -m pip install /opt/torch-tensorrt-builds/torch_tensorrt*+${CU_VERSION}*.whl +${CONDA_RUN} pip install /opt/torch-tensorrt-builds/torch_tensorrt*+${CU_VERSION}*.whl + +# Install additional libraries like timm/transformers which are used during testing +${CONDA_RUN} pip install --pre -r tests/py/requirements.txt --use-deprecated=legacy-resolver + echo -e "Running test script"; \ No newline at end of file From 3f8297eb6301f6b08d93ee68d1aa80a3205b9cb0 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 27 May 2024 15:20:11 -0700 Subject: [PATCH 110/122] chore: updates --- .github/workflows/build-test-linux.yml | 7 ------- .github/workflows/windows-test.yml | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/build-test-linux.yml b/.github/workflows/build-test-linux.yml index 5de2d272ec..04fc69de33 100644 --- a/.github/workflows/build-test-linux.yml +++ b/.github/workflows/build-test-linux.yml @@ -81,7 +81,6 @@ jobs: export LD_LIBRARY_PATH=/usr/lib64:$LD_LIBRARY_PATH pushd . cd tests/modules - ${CONDA_RUN} python -m pip install --pre -r ../py/requirements.txt --use-deprecated=legacy-resolver ${CONDA_RUN} python hub.py popd pushd . @@ -116,7 +115,6 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/dynamo_converters_test_results.xml -n 10 conversion/ popd @@ -145,7 +143,6 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/dynamo_fe_test_results.xml --ir dynamo models/test_models_export.py ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/dyn_models_export.xml --ir dynamo models/test_dyn_models.py popd @@ -175,7 +172,6 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/export_serde_test_results.xml --ir dynamo models/test_export_serde.py popd @@ -204,7 +200,6 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest -n 10 --junitxml=${RUNNER_TEST_RESULTS_DIR}/torch_compile_be_test_results.xml backend/ ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/torch_comple_be_e2e_test_results.xml --ir torch_compile models/test_models.py ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/torch_compile_dyn_models_export.xml --ir torch_compile models/test_dyn_models.py @@ -235,7 +230,6 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/tests_py_dynamo_core_runtime_test_results.xml runtime/ ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/tests_py_dynamo_core_partitioning_test_results.xml partitioning/ ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/tests_py_dynamo_core_lowering_test_results.xml lowering/ @@ -266,6 +260,5 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/core - ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/tests_py_core_test_results.xml . popd diff --git a/.github/workflows/windows-test.yml b/.github/workflows/windows-test.yml index b00b4c3dd1..2d61068b1a 100644 --- a/.github/workflows/windows-test.yml +++ b/.github/workflows/windows-test.yml @@ -118,7 +118,7 @@ jobs: { echo "${SCRIPT}"; } > "user_script" - cat .github/scripts/install-torch-tensorrt-windows.sh user_script > exec_script + cat .github/scripts/install-torch-tensorrt.sh user_script > exec_script - name: Run script uses: ./test-infra/.github/actions/run-script-with-cache with: From 5ce0ee19a7330e5725523d35102ed243efdbdcea Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 27 May 2024 15:21:13 -0700 Subject: [PATCH 111/122] chore: updates --- .github/workflows/build-test-windows.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/build-test-windows.yml b/.github/workflows/build-test-windows.yml index f39286167c..d745e5c7d0 100644 --- a/.github/workflows/build-test-windows.yml +++ b/.github/workflows/build-test-windows.yml @@ -72,7 +72,6 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/dynamo_converters_test_results.xml -n 10 conversion/ popd @@ -98,7 +97,6 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/dynamo_fe_test_results.xml --ir dynamo models/test_models_export.py ${CONDA_RUN} python -m pytest --junitxml=${RUNNER_TEST_RESULTS_DIR}/dyn_models_export.xml --ir dynamo models/test_dyn_models.py popd @@ -125,7 +123,6 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest -n 10 --junitxml=${RUNNER_TEST_RESULTS_DIR}/torch_compile_be_test_results.xml backend/ ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/torch_comple_be_e2e_test_results.xml --ir torch_compile models/test_models.py popd @@ -152,7 +149,6 @@ jobs: export USE_HOST_DEPS=1 pushd . cd tests/py/dynamo - ${CONDA_RUN} python -m pip install --pre -r ../requirements.txt --use-deprecated=legacy-resolver ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/tests_py_dynamo_core_runtime_test_results.xml runtime/ ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/tests_py_dynamo_core_partitioning_test_results.xml partitioning/ ${CONDA_RUN} python -m pytest -n 4 --junitxml=${RUNNER_TEST_RESULTS_DIR}/tests_py_dynamo_core_lowering_test_results.xml lowering/ From 65c5c3e24dc9ea7ba970151ed1c67319b47559ec Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 27 May 2024 20:42:24 -0700 Subject: [PATCH 112/122] chore: updates --- .github/scripts/install-torch-tensorrt.sh | 22 +++----------------- packaging/pre_build_script.sh | 1 - packaging/pre_build_script_windows.sh | 2 +- versions.py | 25 ----------------------- 4 files changed, 4 insertions(+), 46 deletions(-) diff --git a/.github/scripts/install-torch-tensorrt.sh b/.github/scripts/install-torch-tensorrt.sh index 5f33addb74..90f665d20f 100644 --- a/.github/scripts/install-torch-tensorrt.sh +++ b/.github/scripts/install-torch-tensorrt.sh @@ -2,28 +2,12 @@ set -eou pipefail # Source conda so it's available to the script environment source ${BUILD_ENV_FILE} +export EXTRA_INDEX_URL="https://download.pytorch.org/whl/nightly/${CU_VERSION}" -# Install pyyaml first to parse dev_dep_versions.yml in versions.py -${CONDA_RUN} python -m pip install pyyaml - -# Install stable versions of Torch and Torchvision since this is a release branch. -# For main branches, the INDEX_URL will always point to nightly and for RC releases, -# it will contain test tag in the url. -export TORCH_VERSION_CI=$(${CONDA_RUN} python -c "import versions; versions.torch_version()") -export TORCHVISION_VERSION_CI=$(${CONDA_RUN} python -c "import versions; versions.torchvision_version()") -export TENSORRT_VERSION_CI=$(${CONDA_RUN} python -c "import versions; versions.tensorrt_version()") -export INDEX_URL_CI=$(${CONDA_RUN} python -c "import versions; versions.index_url()") - -# Install appropriate torch and torchvision versions for the platform -${CONDA_RUN} pip install torch==${TORCH_VERSION_CI} torchvision==${TORCHVISION_VERSION_CI} --index-url ${INDEX_URL_CI} - -# Install TRT 10 from PyPi -${CONDA_RUN} pip install tensorrt==${TENSORRT_VERSION_CI} +# Install all the dependencies required for Torch-TensorRT +${CONDA_RUN} pip install --pre -r ${PWD}/tests/py/requirements.txt --use-deprecated=legacy-resolver --extra-index-url=${EXTRA_INDEX_URL} # Install Torch-TensorRT ${CONDA_RUN} pip install /opt/torch-tensorrt-builds/torch_tensorrt*+${CU_VERSION}*.whl -# Install additional libraries like timm/transformers which are used during testing -${CONDA_RUN} pip install --pre -r tests/py/requirements.txt --use-deprecated=legacy-resolver - echo -e "Running test script"; \ No newline at end of file diff --git a/packaging/pre_build_script.sh b/packaging/pre_build_script.sh index 59dd3774fe..a8833361e1 100755 --- a/packaging/pre_build_script.sh +++ b/packaging/pre_build_script.sh @@ -3,7 +3,6 @@ # Install dependencies python3 -m pip install pyyaml yum install -y ninja-build gettext -TRT_VERSION=$(python3 -c "import versions; versions.tensorrt_version()") wget https://github.com/bazelbuild/bazelisk/releases/download/v1.17.0/bazelisk-linux-amd64 \ && mv bazelisk-linux-amd64 /usr/bin/bazel \ diff --git a/packaging/pre_build_script_windows.sh b/packaging/pre_build_script_windows.sh index 2004966613..51d3e1499e 100644 --- a/packaging/pre_build_script_windows.sh +++ b/packaging/pre_build_script_windows.sh @@ -1,7 +1,7 @@ python -m pip install -U numpy packaging pyyaml setuptools wheel # Install TRT 10 from PyPi -python -m pip install tensorrt==10.0.0b6 tensorrt-${CU_VERSION::4}-bindings==10.0.0b6 tensorrt-${CU_VERSION::4}-libs==10.0.0b6 --extra-index-url https://pypi.nvidia.com +python -m pip install tensorrt==10.0.1 --extra-index-url https://pypi.nvidia.com choco install bazelisk -y diff --git a/versions.py b/versions.py index cf9b3603bd..7f8d8a445c 100644 --- a/versions.py +++ b/versions.py @@ -10,12 +10,6 @@ __cuda_version__ = "0.0" __cudnn_version__ = "0.0" __tensorrt_version__ = "0.0" -# The following variables are used to install the correct versions of torch & torchvision in CI -__torch_version__ = "0.0" -__torchvision_version__ = "0.0" -# / is necessary at the end -__index_url__ = "https://download.pytorch.org/whl/test/" -DEFAULT_CUDA_INDEX = "cu121" LEADING_V_PATTERN = re.compile("^v") TRAILING_RC_PATTERN = re.compile("-rc[0-9]*$") @@ -111,9 +105,6 @@ def load_dep_info(): global __cuda_version__ global __cudnn_version__ global __tensorrt_version__ - global __torch_version__ - global __torchvision_version__ - global __index_url__ with open("dev_dep_versions.yml", "r") as stream: versions = yaml.safe_load(stream) gpu_arch_version = os.environ.get("CU_VERSION") @@ -121,14 +112,10 @@ def load_dep_info(): __cuda_version__ = ( (gpu_arch_version[2:])[:-1] + "." + (gpu_arch_version[2:])[-1:] ) - __index_url__ = versions["__index_url__"] + gpu_arch_version else: __cuda_version__ = versions["__cuda_version__"] - __index_url__ = versions["__index_url__"] + DEFAULT_CUDA_INDEX __cudnn_version__ = versions["__cudnn_version__"] __tensorrt_version__ = versions["__tensorrt_version__"] - __torch_version__ = versions["__torch_version__"] - __torchvision_version__ = versions["__torchvision_version__"] load_dep_info() @@ -159,15 +146,3 @@ def cudnn_version(): def tensorrt_version(): print(__tensorrt_version__) - - -def torch_version(): - print(__torch_version__) - - -def torchvision_version(): - print(__torchvision_version__) - - -def index_url(): - print(__index_url__) From d99989d5f76eaddb1f1c7015d1e053027a567073 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 27 May 2024 20:45:54 -0700 Subject: [PATCH 113/122] chore: updates --- .github/scripts/install-torch-tensorrt.sh | 2 +- tests/py/requirements.txt | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/scripts/install-torch-tensorrt.sh b/.github/scripts/install-torch-tensorrt.sh index 90f665d20f..f1b39e92d6 100644 --- a/.github/scripts/install-torch-tensorrt.sh +++ b/.github/scripts/install-torch-tensorrt.sh @@ -2,7 +2,7 @@ set -eou pipefail # Source conda so it's available to the script environment source ${BUILD_ENV_FILE} -export EXTRA_INDEX_URL="https://download.pytorch.org/whl/nightly/${CU_VERSION}" +export EXTRA_INDEX_URL="https://download.pytorch.org/whl/test/${CU_VERSION}" # Install all the dependencies required for Torch-TensorRT ${CONDA_RUN} pip install --pre -r ${PWD}/tests/py/requirements.txt --use-deprecated=legacy-resolver --extra-index-url=${EXTRA_INDEX_URL} diff --git a/tests/py/requirements.txt b/tests/py/requirements.txt index 0df01cb0f2..db2c968a6a 100644 --- a/tests/py/requirements.txt +++ b/tests/py/requirements.txt @@ -1,6 +1,12 @@ +# This file is specifically to install correct version of libraries during CI testing. +# The index url for torch & torchvision libs is configured in install-torch-tensorrt.sh based on CUDA version pytest>=8.2.1 pytest-xdist>=3.6.1 +torch==2.3.0 +torchvision==0.18.0 +--extra-index-url https://pypi.ngc.nvidia.com +pyyaml +tensorrt==10.0.1 timm>=1.0.3 transformers==4.39.3 -parameterized>=0.2.0 -expecttest==0.1.6 \ No newline at end of file +parameterized>=0.2.0 \ No newline at end of file From 99dfbdc0b0927d32295977eb66b22d5f2ff9748c Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 27 May 2024 21:01:03 -0700 Subject: [PATCH 114/122] chore: updates --- tests/py/requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/py/requirements.txt b/tests/py/requirements.txt index db2c968a6a..ab718475f1 100644 --- a/tests/py/requirements.txt +++ b/tests/py/requirements.txt @@ -1,7 +1,9 @@ # This file is specifically to install correct version of libraries during CI testing. # The index url for torch & torchvision libs is configured in install-torch-tensorrt.sh based on CUDA version +# networkx library issue: https://discuss.pytorch.org/t/installing-pytorch-under-python-3-8-question-about-networkx-version/196740 pytest>=8.2.1 pytest-xdist>=3.6.1 +networkx==2.8.8 torch==2.3.0 torchvision==0.18.0 --extra-index-url https://pypi.ngc.nvidia.com From ad996a55e740511ade43447218b3e6362aa8c4fc Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Mon, 27 May 2024 21:47:51 -0700 Subject: [PATCH 115/122] chore: updates --- tests/py/requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/py/requirements.txt b/tests/py/requirements.txt index ab718475f1..bdb8ed48bc 100644 --- a/tests/py/requirements.txt +++ b/tests/py/requirements.txt @@ -11,4 +11,5 @@ pyyaml tensorrt==10.0.1 timm>=1.0.3 transformers==4.39.3 -parameterized>=0.2.0 \ No newline at end of file +parameterized>=0.2.0 +expecttest==0.1.6 \ No newline at end of file From 6ada35182c3ff3fd341e74d352fd0c953c178ba5 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 28 May 2024 10:42:49 -0700 Subject: [PATCH 116/122] chore: updates --- .github/scripts/install-torch-tensorrt.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/install-torch-tensorrt.sh b/.github/scripts/install-torch-tensorrt.sh index f1b39e92d6..a9bb2de410 100644 --- a/.github/scripts/install-torch-tensorrt.sh +++ b/.github/scripts/install-torch-tensorrt.sh @@ -8,6 +8,6 @@ export EXTRA_INDEX_URL="https://download.pytorch.org/whl/test/${CU_VERSION}" ${CONDA_RUN} pip install --pre -r ${PWD}/tests/py/requirements.txt --use-deprecated=legacy-resolver --extra-index-url=${EXTRA_INDEX_URL} # Install Torch-TensorRT -${CONDA_RUN} pip install /opt/torch-tensorrt-builds/torch_tensorrt*+${CU_VERSION}*.whl +${CONDA_RUN} pip install /opt/torch-tensorrt-builds/torch_tensorrt*.whl echo -e "Running test script"; \ No newline at end of file From 88fd7ee68aa7a0be46e00295ec67f475f224a9bd Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 28 May 2024 12:08:13 -0700 Subject: [PATCH 117/122] chore: fixes --- .github/scripts/install-torch-tensorrt.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/install-torch-tensorrt.sh b/.github/scripts/install-torch-tensorrt.sh index a9bb2de410..955f4d830a 100644 --- a/.github/scripts/install-torch-tensorrt.sh +++ b/.github/scripts/install-torch-tensorrt.sh @@ -8,6 +8,6 @@ export EXTRA_INDEX_URL="https://download.pytorch.org/whl/test/${CU_VERSION}" ${CONDA_RUN} pip install --pre -r ${PWD}/tests/py/requirements.txt --use-deprecated=legacy-resolver --extra-index-url=${EXTRA_INDEX_URL} # Install Torch-TensorRT -${CONDA_RUN} pip install /opt/torch-tensorrt-builds/torch_tensorrt*.whl +${CONDA_RUN} pip install ${RUNNER_ARTIFACT_DIR}/torch_tensorrt*.whl echo -e "Running test script"; \ No newline at end of file From 2511095105edd048ecc0a63a7702286c6db9b550 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Tue, 28 May 2024 13:43:37 -0700 Subject: [PATCH 118/122] chore: updates --- .github/scripts/install-torch-tensorrt.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/scripts/install-torch-tensorrt.sh b/.github/scripts/install-torch-tensorrt.sh index 955f4d830a..176a5b5ce4 100644 --- a/.github/scripts/install-torch-tensorrt.sh +++ b/.github/scripts/install-torch-tensorrt.sh @@ -3,11 +3,15 @@ set -eou pipefail # Source conda so it's available to the script environment source ${BUILD_ENV_FILE} export EXTRA_INDEX_URL="https://download.pytorch.org/whl/test/${CU_VERSION}" - +export PLATFORM=$(${CONDA_RUN} python -c "import sys; print(sys.platform)") # Install all the dependencies required for Torch-TensorRT ${CONDA_RUN} pip install --pre -r ${PWD}/tests/py/requirements.txt --use-deprecated=legacy-resolver --extra-index-url=${EXTRA_INDEX_URL} -# Install Torch-TensorRT -${CONDA_RUN} pip install ${RUNNER_ARTIFACT_DIR}/torch_tensorrt*.whl +# Install Torch-TensorRT via pre-built wheels. On windows, the location of wheels is not fixed. +if ${PLATFORM} == "win32"; then + ${CONDA_RUN} pip install ${RUNNER_ARTIFACT_DIR}/torch_tensorrt*.whl +else + ${CONDA_RUN} pip install /opt/torch-tensorrt-builds/torch_tensorrt*.whl +fi echo -e "Running test script"; \ No newline at end of file From 5346a4542c4be26ac7873943dcafe0b2cec01d15 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Wed, 29 May 2024 11:54:35 -0700 Subject: [PATCH 119/122] chore: updates --- .github/scripts/install-torch-tensorrt.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/install-torch-tensorrt.sh b/.github/scripts/install-torch-tensorrt.sh index 176a5b5ce4..4833fd12f1 100644 --- a/.github/scripts/install-torch-tensorrt.sh +++ b/.github/scripts/install-torch-tensorrt.sh @@ -8,7 +8,7 @@ export PLATFORM=$(${CONDA_RUN} python -c "import sys; print(sys.platform)") ${CONDA_RUN} pip install --pre -r ${PWD}/tests/py/requirements.txt --use-deprecated=legacy-resolver --extra-index-url=${EXTRA_INDEX_URL} # Install Torch-TensorRT via pre-built wheels. On windows, the location of wheels is not fixed. -if ${PLATFORM} == "win32"; then +if [[ "$PLATFORM" == "win32" ]]; then ${CONDA_RUN} pip install ${RUNNER_ARTIFACT_DIR}/torch_tensorrt*.whl else ${CONDA_RUN} pip install /opt/torch-tensorrt-builds/torch_tensorrt*.whl From d284b8f245217ac8343de77cbb16b0d70da07d6f Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Wed, 29 May 2024 13:23:04 -0700 Subject: [PATCH 120/122] chore: updates --- .github/workflows/windows-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows-test.yml b/.github/workflows/windows-test.yml index 2d61068b1a..b00b4c3dd1 100644 --- a/.github/workflows/windows-test.yml +++ b/.github/workflows/windows-test.yml @@ -118,7 +118,7 @@ jobs: { echo "${SCRIPT}"; } > "user_script" - cat .github/scripts/install-torch-tensorrt.sh user_script > exec_script + cat .github/scripts/install-torch-tensorrt-windows.sh user_script > exec_script - name: Run script uses: ./test-infra/.github/actions/run-script-with-cache with: From c71c01785642340e41624be11817291fdabda591 Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Wed, 29 May 2024 13:23:18 -0700 Subject: [PATCH 121/122] chore: updates --- .github/scripts/install-torch-tensorrt-windows.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/scripts/install-torch-tensorrt-windows.sh diff --git a/.github/scripts/install-torch-tensorrt-windows.sh b/.github/scripts/install-torch-tensorrt-windows.sh new file mode 100644 index 0000000000..2c746ef435 --- /dev/null +++ b/.github/scripts/install-torch-tensorrt-windows.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -eou pipefail +# Source conda so it's available to the script environment +source ${BUILD_ENV_FILE} +export EXTRA_INDEX_URL="https://download.pytorch.org/whl/test/${CU_VERSION}" +export PLATFORM=$(${CONDA_RUN} python -c "import sys; print(sys.platform)") +# Install all the dependencies required for Torch-TensorRT +${CONDA_RUN} pip install --pre -r ${PWD}/tests/py/requirements.txt --use-deprecated=legacy-resolver --extra-index-url=${EXTRA_INDEX_URL} + +# Install Torch-TensorRT via pre-built wheels. On windows, the location of wheels is not fixed. +${CONDA_RUN} pip install ${RUNNER_ARTIFACT_DIR}/torch_tensorrt*.whl + +echo -e "Running test script"; \ No newline at end of file From a983064318c42d0f110e36dc86fce82d1270c9ee Mon Sep 17 00:00:00 2001 From: Dheeraj Peri Date: Wed, 29 May 2024 13:27:51 -0700 Subject: [PATCH 122/122] chore: updates --- .github/scripts/install-torch-tensorrt-windows.sh | 1 - .github/scripts/install-torch-tensorrt.sh | 7 +------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/scripts/install-torch-tensorrt-windows.sh b/.github/scripts/install-torch-tensorrt-windows.sh index 2c746ef435..bd8041a2e2 100644 --- a/.github/scripts/install-torch-tensorrt-windows.sh +++ b/.github/scripts/install-torch-tensorrt-windows.sh @@ -3,7 +3,6 @@ set -eou pipefail # Source conda so it's available to the script environment source ${BUILD_ENV_FILE} export EXTRA_INDEX_URL="https://download.pytorch.org/whl/test/${CU_VERSION}" -export PLATFORM=$(${CONDA_RUN} python -c "import sys; print(sys.platform)") # Install all the dependencies required for Torch-TensorRT ${CONDA_RUN} pip install --pre -r ${PWD}/tests/py/requirements.txt --use-deprecated=legacy-resolver --extra-index-url=${EXTRA_INDEX_URL} diff --git a/.github/scripts/install-torch-tensorrt.sh b/.github/scripts/install-torch-tensorrt.sh index 4833fd12f1..90db1d5fca 100644 --- a/.github/scripts/install-torch-tensorrt.sh +++ b/.github/scripts/install-torch-tensorrt.sh @@ -3,15 +3,10 @@ set -eou pipefail # Source conda so it's available to the script environment source ${BUILD_ENV_FILE} export EXTRA_INDEX_URL="https://download.pytorch.org/whl/test/${CU_VERSION}" -export PLATFORM=$(${CONDA_RUN} python -c "import sys; print(sys.platform)") # Install all the dependencies required for Torch-TensorRT ${CONDA_RUN} pip install --pre -r ${PWD}/tests/py/requirements.txt --use-deprecated=legacy-resolver --extra-index-url=${EXTRA_INDEX_URL} # Install Torch-TensorRT via pre-built wheels. On windows, the location of wheels is not fixed. -if [[ "$PLATFORM" == "win32" ]]; then - ${CONDA_RUN} pip install ${RUNNER_ARTIFACT_DIR}/torch_tensorrt*.whl -else - ${CONDA_RUN} pip install /opt/torch-tensorrt-builds/torch_tensorrt*.whl -fi +${CONDA_RUN} pip install /opt/torch-tensorrt-builds/torch_tensorrt*.whl echo -e "Running test script"; \ No newline at end of file