From dc03ce0278d188ebdaf05c5dcd8995412ab75c43 Mon Sep 17 00:00:00 2001 From: Changming Sun Date: Tue, 17 Sep 2019 10:55:31 -0700 Subject: [PATCH] New OP: CDist (#1808) Add a new op for scikit-learn converter. It's for scikit's cdist function: https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.distance.cdist.html Will add docs and shape-inference function later. Will convert it to an ONNX function before pushing into ONNX. --- onnxruntime/contrib_ops/cpu/cdist.cc | 16 ++ onnxruntime/contrib_ops/cpu/cdist.h | 155 ++++++++++++++++++ .../contrib_ops/cpu_contrib_kernels.cc | 4 + .../core/graph/contrib_ops/contrib_defs.cc | 17 ++ onnxruntime/core/util/distance.h | 55 +++++++ onnxruntime/core/util/math_cpuonly.h | 46 +++--- onnxruntime/test/framework/distance_test.cc | 25 +++ onnxruntime/test/onnx/gen_test_models.py | 80 ++++++++- .../linux/docker/scripts/install_ubuntu.sh | 1 + 9 files changed, 372 insertions(+), 27 deletions(-) create mode 100644 onnxruntime/contrib_ops/cpu/cdist.cc create mode 100644 onnxruntime/contrib_ops/cpu/cdist.h create mode 100644 onnxruntime/core/util/distance.h create mode 100644 onnxruntime/test/framework/distance_test.cc diff --git a/onnxruntime/contrib_ops/cpu/cdist.cc b/onnxruntime/contrib_ops/cpu/cdist.cc new file mode 100644 index 0000000000..d97f32846e --- /dev/null +++ b/onnxruntime/contrib_ops/cpu/cdist.cc @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "cdist.h" + +namespace onnxruntime { +namespace contrib { +#define DEFINE_KERNEL(data_type) \ + ONNX_OPERATOR_TYPED_KERNEL_EX(CDist, kMSDomain, 1, data_type, kCpuExecutionProvider, \ + KernelDefBuilder().TypeConstraint("T", DataTypeImpl::GetTensorType()), \ + CDist); +DEFINE_KERNEL(float); +DEFINE_KERNEL(double); + +} // namespace contrib +} // namespace onnxruntime \ No newline at end of file diff --git a/onnxruntime/contrib_ops/cpu/cdist.h b/onnxruntime/contrib_ops/cpu/cdist.h new file mode 100644 index 0000000000..9ffaf2fce6 --- /dev/null +++ b/onnxruntime/contrib_ops/cpu/cdist.h @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "core/common/common.h" +#include "core/util/distance.h" +#include "core/framework/op_kernel.h" +#include "core/framework/op_kernel_context_internal.h" +#include "core/util/math_cpuonly.h" +#include "assert.h" +#ifndef USE_OPENMP +#include "core/util/eigen_common_wrapper.h" +#endif + +namespace onnxruntime { +namespace contrib { + +// https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.distance.cdist.html +//\param a: matrix with shape of[ma,n] +//\param b: matrix with shape of[mb,n] +//\param dest: matrix with shape of [ma,mb] +template +void cdist_single_threaded(const T* a, const T* b, T* dest, size_t ma, size_t mb, size_t n) { + ElemFunc f; + for (size_t i = 0; i != ma; ++i) { + const T* a1 = a + n * i; + for (size_t j = 0; j != mb; ++j) { + const T* b1 = b + n * j; + *dest++ = f(a1, b1, n); + } + } +} + +template +class CDistOneBlock { + public: + const T* a; + const T* b; + T* dest; + size_t mb; + size_t n; + + CDistOneBlock(const T* a1, const T* b1, T* dest1, size_t mb1, size_t n1) + : a(a1), b(b1), dest(dest1), mb(mb1), n(n1) {} + + void operator()(Eigen::Index start, Eigen::Index end) { + Eigen::Index mb_local = mb; + Eigen::Index i = start / mb_local; + Eigen::Index j = start - i * mb_local; + assert(i * mb_local + j == start); + Eigen::Index i_end = end / mb_local; + Eigen::Index j_end = end - i_end * mb_local; + assert(i_end * mb_local + j_end == end); + + T* dest_local = dest + start; + ElemFunc f; + const T* a1 = a + n * i; + for (; i != i_end; ++i) { + a1 = a + n * i; + for (; j != mb_local; ++j) { + const T* b1 = b + n * j; + *dest_local++ = f(a1, b1, n); + } + j = 0; + } + a1 = a + n * i; + for (j = 0; j != j_end; ++j) { + const T* b1 = b + n * j; + *dest_local++ = f(a1, b1, n); + } + assert(dest_local == dest + end); + } +}; + +template +void cdist(const T* a, const T* b, T* dest, size_t ma, size_t mb, size_t n, concurrency::ThreadPool* tp) { +#ifndef USE_OPENMP + if (tp == nullptr) { +#else + (void)tp; +#endif + return cdist_single_threaded(a, b, dest, ma, mb, n); +#ifndef USE_OPENMP + } + Eigen::ThreadPoolDevice device(&tp->GetHandler(), tp->NumThreads()); + device.parallelFor(ma * mb, Eigen::TensorOpCost(0, 0, static_cast(3 * n)), + CDistOneBlock(a, b, dest, mb, n)); +#endif +} + +template +class CDist final : public OpKernel { + private: + typedef void (*DistFunc)(const T* a, const T* b, T* dest, size_t ma, size_t mb, size_t n, + concurrency::ThreadPool* tp); + enum { EUCLIDEAN, SQEUCLIDEAN } mode_; + + public: + CDist(const OpKernelInfo& info) : OpKernel(info) { + std::string metric; + ORT_ENFORCE(info.GetAttr("metric", &metric).IsOK()); + if (metric.compare("sqeuclidean") == 0) + mode_ = SQEUCLIDEAN; + else if (metric.compare("euclidean") == 0) { + mode_ = EUCLIDEAN; + } else + ORT_NOT_IMPLEMENTED(); + } + + common::Status Compute(OpKernelContext* context) const override { + auto ctx_internal = static_cast(context); + concurrency::ThreadPool* tp = ctx_internal->GetOperatorThreadPool(); + + assert(context->InputCount() == 2); + const Tensor* A = context->Input(0); + const Tensor* B = context->Input(1); + const TensorShape& shape_a = A->Shape(); + const TensorShape& shape_b = B->Shape(); + if (shape_a.NumDimensions() != 2 || shape_a[1] <= 0) { + return ORT_MAKE_STATUS(ONNXRUNTIME, FAIL, "The first input of CDist kernel has wrong shape: ", shape_a); + } + + if (shape_b.NumDimensions() != 2) { + return ORT_MAKE_STATUS(ONNXRUNTIME, FAIL, "The second input of CDist kernel has wrong shape: ", shape_b); + } + if (shape_a[1] != shape_b[1]) { + return ORT_MAKE_STATUS(ONNXRUNTIME, FAIL, "Input shape dimensions mismatch:", shape_a, " and ", shape_b); + } + + TensorShape output_shape = {shape_a[0], shape_b[0]}; + Tensor* C = context->Output(0, output_shape); + T* output = C->MutableData(); + switch (mode_) { + case EUCLIDEAN: + if (shape_a[1] >= 8) + cdist >(A->Data(), B->Data(), output, shape_a[0], shape_b[0], shape_a[1], tp); + else // for smaller vector size, a raw loop is better + cdist >(A->Data(), B->Data(), output, shape_a[0], shape_b[0], shape_a[1], tp); + break; + case SQEUCLIDEAN: + if (shape_a[1] >= 8) + cdist >(A->Data(), B->Data(), output, shape_a[0], shape_b[0], shape_a[1], + tp); + else // for smaller vector size, a raw loop is better + cdist >(A->Data(), B->Data(), output, shape_a[0], shape_b[0], shape_a[1], tp); + break; + default: + return Status(ONNXRUNTIME, NOT_IMPLEMENTED); + } + return Status::OK(); + } +}; +} // namespace contrib +} // namespace onnxruntime \ No newline at end of file diff --git a/onnxruntime/contrib_ops/cpu_contrib_kernels.cc b/onnxruntime/contrib_ops/cpu_contrib_kernels.cc index 3d05e8a28f..ec454f04bf 100644 --- a/onnxruntime/contrib_ops/cpu_contrib_kernels.cc +++ b/onnxruntime/contrib_ops/cpu_contrib_kernels.cc @@ -27,6 +27,8 @@ class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, uint8_t, DequantizeLinear); class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, int8_t, DequantizeLinear); class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, uint8_t, QuantizeLinear); +class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, float, CDist); +class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, double, CDist); // This section includes all opkernel declarations for former experimental ops which have now been removed from onnx. // To maintain backward compatibility these are added as contrib ops. @@ -100,6 +102,8 @@ void RegisterCpuContribKernels(KernelRegistry& kernel_registry) { BuildKernelCreateInfo, BuildKernelCreateInfo, BuildKernelCreateInfo, + BuildKernelCreateInfo, + BuildKernelCreateInfo, // These ops were experimental ops in onnx domain which have been removed now. We add them here as // contrib ops to main backward compatibility diff --git a/onnxruntime/core/graph/contrib_ops/contrib_defs.cc b/onnxruntime/core/graph/contrib_ops/contrib_defs.cc index 1a8adf6fc1..31af8a5f61 100644 --- a/onnxruntime/core/graph/contrib_ops/contrib_defs.cc +++ b/onnxruntime/core/graph/contrib_ops/contrib_defs.cc @@ -1648,6 +1648,23 @@ Example 4: output_counts = [1, 2, 2, 1] )DOC"); + //see:https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.distance.cdist.html + ONNX_CONTRIB_OPERATOR_SCHEMA(CDist) + .SetDomain(kMSDomain) + .SinceVersion(1) + .Attr("metric", + "The distance metric to use. If a string, the distance function can be \"braycurtis\", \"canberra\", " + "\"chebyshev\", \"cityblock\", \"correlation\", \"cosine\", \"dice\", \"euclidean\", \"hamming\", \"jaccard\", " + "\"jensenshannon\", \"kulsinski\", \"mahalanobis\", \"matching\", \"minkowski\", \"rogerstanimoto\", \"russellrao\", " + "\"seuclidean\", \"sokalmichener\", \"sokalsneath\", \"sqeuclidean\", \"wminkowski\", \"yule\".", + AttributeProto::STRING, std::string("sqeuclidean")) + .Input(0, "A", "2D matrix with shape (M,N)", "T") + .Input(1, "B", "2D matrix with shape (K,N)", "T") + .Output(0, "C", + "A 2D Matrix that represents the distance between each pair of the two collections of inputs.", + "T") + .TypeConstraint("T", {"tensor(float)", "tensor(double)"}, "Constrains input to only numeric types."); + ONNX_CONTRIB_OPERATOR_SCHEMA(CropAndResize) .SetDomain(kMSDomain) .SinceVersion(1) diff --git a/onnxruntime/core/util/distance.h b/onnxruntime/core/util/distance.h new file mode 100644 index 0000000000..02d6147df5 --- /dev/null +++ b/onnxruntime/core/util/distance.h @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +#include +#include "math_cpuonly.h" + +namespace onnxruntime { + +// Computes the squared Euclidean distance between the vectors. +template +class Sqeuclidean { + public: + T operator()(const T* a1, const T* b1, size_t n) const { + // if n is too small, Eigen is much slower than a plain loop + T sum = 0; + for (size_t k = 0; k != n; ++k) { + const T t = a1[k] - b1[k]; + sum += t * t; + } + return sum; + } +}; + +// Computes the Euclidean distance between the vectors. +template +class Euclidean { + public: + T operator()(const T* a1, const T* b1, size_t n) const { + // if n is too small, Eigen is much slower than a plain loop + T sum = 0; + for (size_t k = 0; k != n; ++k) { + const T t = a1[k] - b1[k]; + sum += t * t; + } + return std::sqrt(sum); + } +}; + +template +class SqeuclideanWithEigen { + public: + T operator()(const T* a1, const T* b1, size_t n) const { + return (ConstEigenVectorMap(a1, n) - ConstEigenVectorMap(b1, n)).array().square().sum(); + } +}; + +template +class EuclideanWithEigen { + public: + T operator()(const T* a1, const T* b1, size_t n) const { + return std::sqrt((ConstEigenVectorMap(a1, n) - ConstEigenVectorMap(b1, n)).array().square().sum()); + } +}; +} // namespace onnxruntime \ No newline at end of file diff --git a/onnxruntime/core/util/math_cpuonly.h b/onnxruntime/core/util/math_cpuonly.h index 3865d4b990..66a0d4d0e4 100644 --- a/onnxruntime/core/util/math_cpuonly.h +++ b/onnxruntime/core/util/math_cpuonly.h @@ -1,18 +1,18 @@ /** -* Copyright (c) 2016-present, Facebook, Inc. -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ + * Copyright (c) 2016-present, Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ // Modifications Copyright (c) Microsoft. #pragma once @@ -35,7 +35,7 @@ #endif #include "Eigen/Dense" - +#include "core/framework/tensor.h" namespace onnxruntime { // common Eigen types that we will often use @@ -56,16 +56,18 @@ using ConstEigenVectorMap = Eigen::Map template using ConstEigenVectorArrayMap = Eigen::Map>; template -using EigenMatrixMapRowMajor = Eigen::Map< - Eigen::Matrix>; +using EigenMatrixMapRowMajor = Eigen::Map>; template -using ConstEigenMatrixMapRowMajor = Eigen::Map< - const Eigen::Matrix>; +using ConstEigenMatrixMapRowMajor = Eigen::Map>; template -auto EigenMap(Tensor& t) { return EigenVectorMap(t.template MutableData(), t.Shape().Size()); } +auto EigenMap(Tensor& t) { + return EigenVectorMap(t.template MutableData(), t.Shape().Size()); +} template -auto EigenMap(const Tensor& t) { return ConstEigenVectorMap(t.template Data(), t.Shape().Size()); } +auto EigenMap(const Tensor& t) { + return ConstEigenVectorMap(t.template Data(), t.Shape().Size()); +} class CPUMathUtil { public: @@ -75,7 +77,7 @@ class CPUMathUtil { static CPUMathUtil p; return p; } - //todo: the random generate interface. + // todo: the random generate interface. private: CPUMathUtil() = default; }; diff --git a/onnxruntime/test/framework/distance_test.cc b/onnxruntime/test/framework/distance_test.cc new file mode 100644 index 0000000000..ce45936e1e --- /dev/null +++ b/onnxruntime/test/framework/distance_test.cc @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "core/util/distance.h" +#include +using testing::Types; + +namespace onnxruntime { + +template +class SqeuclideanTest : public ::testing::Test { + public: + T func; +}; + +using MyTypes = Types, SqeuclideanWithEigen >; + +TYPED_TEST_CASE(SqeuclideanTest, MyTypes); + +TYPED_TEST(SqeuclideanTest, test1) { + double a = 10; + double b = 5; + ASSERT_DOUBLE_EQ(25, this->func(&a, &b, 1)); +} +} // namespace onnxruntime \ No newline at end of file diff --git a/onnxruntime/test/onnx/gen_test_models.py b/onnxruntime/test/onnx/gen_test_models.py index afdbf5507c..6259f40959 100644 --- a/onnxruntime/test/onnx/gen_test_models.py +++ b/onnxruntime/test/onnx/gen_test_models.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. +# Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - import onnx import numpy as np import os @@ -10,12 +9,27 @@ from onnx import numpy_helper from onnx import helper from onnx import utils from onnx import AttributeProto, TensorProto, GraphProto +from scipy.spatial import distance + def parse_arguments(): parser = argparse.ArgumentParser() parser.add_argument("--output_dir", required=True, help="Path to the build directory.") return parser.parse_args() + +def write_config(model_dir): + with open(os.path.join(model_dir,"config.txt"),"w") as f: + f.write("per_sample_tolerance:1e-6\n") + f.write("relative_per_sample_tolerance:1e-6\n") + +def write_tensor(f, c,input_name=None): + tensor = numpy_helper.from_array(c) + if input_name: + tensor.name = input_name + body = tensor.SerializeToString() + f.write(body) + def generate_abs_op_test(type, X, top_test_folder): for is_raw in [True, False]: if is_raw: @@ -35,7 +49,7 @@ def generate_abs_op_test(type, X, top_test_folder): node_def = helper.make_node('Abs', inputs=['X'], outputs=['Y']) # Create the graph (GraphProto) - graph_def = helper.make_graph( [node_def], 'test-model', [X_INFO], [Y], [tensor_x]) + graph_def = helper.make_graph([node_def], 'test-model', [X_INFO], [Y], [tensor_x]) # Create the model (ModelProto) model_def = helper.make_model(graph_def, producer_name='onnx-example') #final_model = onnx.utils.polish_model(model_def) @@ -59,7 +73,7 @@ def generate_size_op_test(type, X, test_folder): node_def = helper.make_node('Size', inputs=['X'], outputs=['Y']) # Create the graph (GraphProto) - graph_def = helper.make_graph( [node_def], 'test-model', [X_INFO], [Y], [tensor_x]) + graph_def = helper.make_graph([node_def], 'test-model', [X_INFO], [Y], [tensor_x]) # Create the model (ModelProto) model_def = helper.make_model(graph_def, producer_name='onnx-example') final_model = onnx.utils.polish_model(model_def) @@ -69,6 +83,28 @@ def generate_size_op_test(type, X, test_folder): with open(os.path.join(data_dir,"output_0.pb"),"wb") as f: f.write(expected_output_tensor.SerializeToString()) +def generate_reducesum_op_test(X, test_folder): + type = onnx.mapping.NP_TYPE_TO_TENSOR_TYPE[X.dtype] + data_dir = os.path.join(test_folder,"test_data_0") + os.makedirs(data_dir, exist_ok=True) + # Create one output (ValueInfoProto) + Y = helper.make_tensor_value_info('Y', type, []) + X_INFO = helper.make_tensor_value_info('X', type, X.shape) + tensor_x = onnx.helper.make_tensor(name='X', data_type=type, dims=X.shape, vals=X.ravel(),raw=False) + # Create a node (NodeProto) + node_def = helper.make_node('ReduceSum', inputs=['X'], outputs=['Y'], keepdims=0) + + # Create the graph (GraphProto) + graph_def = helper.make_graph([node_def], 'test-model', [X_INFO], [Y], [tensor_x]) + # Create the model (ModelProto) + model_def = helper.make_model(graph_def, producer_name='onnx-example') + final_model = onnx.utils.polish_model(model_def) + onnx.save(final_model, os.path.join(test_folder, 'model.onnx')) + expected_output_array = np.sum(X) + expected_output_tensor = numpy_helper.from_array(expected_output_array) + with open(os.path.join(data_dir,"output_0.pb"),"wb") as f: + f.write(expected_output_tensor.SerializeToString()) + def test_abs(output_dir): generate_abs_op_test(TensorProto.FLOAT, np.random.randn(3, 4, 5).astype(np.float32), os.path.join(output_dir,'test_abs_float')) generate_abs_op_test(TensorProto.DOUBLE, np.random.randn(3, 4, 5).astype(np.float64), os.path.join(output_dir,'test_abs_double')) @@ -83,14 +119,48 @@ def test_abs(output_dir): number_info = np.iinfo(np.uint64) generate_abs_op_test(TensorProto.UINT64, np.uint64([0, 1, 20, number_info.max]), os.path.join(output_dir, 'test_abs_uint64')) +def test_reducesum(output_dir): + generate_reducesum_op_test(np.random.randn(3, 4, 5).astype(np.float32), os.path.join(output_dir, 'test_reducesum_random')) + def test_size(output_dir): generate_size_op_test(TensorProto.FLOAT, np.random.randn(100, 3000, 10).astype(np.float32), os.path.join(output_dir,'test_size_float')) generate_size_op_test(TensorProto.STRING, np.array(['abc', 'xy'], dtype=np.bytes_), os.path.join(output_dir,'test_size_string')) + +def gen_cdist_test(output_dir, dtype, M, N, K): + for mode in ['euclidean', 'sqeuclidean']: + test_folder = os.path.join(output_dir,"test_cdist_%s_%s_%d_%d_%d" % (dtype.__name__, mode, M,N,K)) + data_dir = os.path.join(test_folder, "test_data_0") + os.makedirs(data_dir, exist_ok=True) + a = np.random.randn(M, K).astype(dtype) + b = np.random.randn(N, K).astype(dtype) + type = onnx.mapping.NP_TYPE_TO_TENSOR_TYPE[a.dtype] + c = distance.cdist(a, b, mode).astype(dtype) + node_def = helper.make_node('CDist', inputs=['A','B'], outputs=['C'],domain="com.microsoft", metric=mode) + graph_def = helper.make_graph([node_def], 'test-model', [helper.make_tensor_value_info('A', type, a.shape), + helper.make_tensor_value_info('B', type, b.shape)], + [helper.make_tensor_value_info('C', type, c.shape)]) + model_def = helper.make_model(graph_def, producer_name='onnx-example',opset_imports=[helper.make_opsetid("com.microsoft", 1)]) + onnx.save(model_def, os.path.join(test_folder, 'model.onnx')) + with open(os.path.join(data_dir,"input_0.pb"),"wb") as f: + write_tensor(f, a, "A") + with open(os.path.join(data_dir,"input_1.pb"),"wb") as f: + write_tensor(f, b, "B") + with open(os.path.join(data_dir,"output_0.pb"),"wb") as f: + write_tensor(f, c, "C") + write_config(test_folder) + +def test_cdist(output_dir): + for dtype in [np.float32, np.float64] : + gen_cdist_test(output_dir, dtype, 1000, 2000, 500) + gen_cdist_test(output_dir, dtype, 1000, 2000, 1) + gen_cdist_test(output_dir, dtype, 1, 1, 1) + args = parse_arguments() os.makedirs(args.output_dir,exist_ok=True) test_abs(args.output_dir) test_size(args.output_dir) - +test_reducesum(args.output_dir) +test_cdist(args.output_dir) diff --git a/tools/ci_build/github/linux/docker/scripts/install_ubuntu.sh b/tools/ci_build/github/linux/docker/scripts/install_ubuntu.sh index 71c2132512..4dc4a070e2 100755 --- a/tools/ci_build/github/linux/docker/scripts/install_ubuntu.sh +++ b/tools/ci_build/github/linux/docker/scripts/install_ubuntu.sh @@ -85,6 +85,7 @@ fi if [ $DEVICE_TYPE = "Normal" ]; then /usr/bin/python${PYTHON_VER} -m pip install --upgrade --force-reinstall sympy==1.1.1 fi +/usr/bin/python${PYTHON_VER} -m pip install --upgrade scipy rm -rf /var/lib/apt/lists/* if [ $DEVICE_TYPE = "Normal" ]; then