Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/using_cloud_storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ openvino/model_server:latest \
Add the S3 path as the model_path and pass the credentials as environment variables to the Docker container.
`S3_ENDPOINT` is optional for Amazon S3 storage and mandatory for MinIO and other S3-compatible storage types.

When `S3_ENDPOINT` is set without a scheme, OVMS uses `http://` by default for compatibility with MinIO and other S3-compatible deployments. To force TLS, provide the endpoint as `https://host:port` explicitly.

Example command with `s3://<bucket>/<model_path>:`

```bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,12 @@ class CustomNodeLibraryInternalManager {

template <typename T>
bool get_buffer(ovms::custom_nodes_common::CustomNodeLibraryInternalManager* internalManager, T** buffer, const char* buffersQueueName, uint64_t byte_size) {
*buffer = nullptr;
auto buffersQueue = internalManager->getBuffersQueue(buffersQueueName);
if (!(buffersQueue == nullptr)) {
if (buffersQueue != nullptr && byte_size <= buffersQueue->getSingleBufferSize()) {
*buffer = static_cast<T*>(buffersQueue->getBuffer());
}
if (*buffer == nullptr || buffersQueue == nullptr) {
if (*buffer == nullptr) {
*buffer = (T*)malloc(byte_size);
if (*buffer == nullptr) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,9 @@ DLL_PUBLIC int execute(const struct CustomNodeTensor* inputs, int inputsCount, s
NODE_ASSERT(boxes.size() == confidences.size(), "boxes and confidences are not equal length");
if (boxes.size() > maxOutputBatch) {
boxes.resize(maxOutputBatch);
detections.resize(maxOutputBatch);
confidences.resize(maxOutputBatch);
labelIds.resize(maxOutputBatch);
}

CustomNodeLibraryInternalManager* internalManager = static_cast<CustomNodeLibraryInternalManager*>(customNodeLibraryInternalManager);
Expand Down
24 changes: 18 additions & 6 deletions src/filesystem/s3filesystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ namespace ovms {
namespace s3 = Aws::S3;
namespace fs = std::filesystem;

std::pair<std::string, Aws::Http::Scheme> S3FileSystem::parseEndpoint(const std::string& endpoint) {
std::string normalized = endpoint;
auto scheme = Aws::Http::Scheme::HTTP;

if (normalized.rfind("http://", 0) == 0) {
normalized = normalized.substr(7);
scheme = Aws::Http::Scheme::HTTP;
} else if (normalized.rfind("https://", 0) == 0) {
normalized = normalized.substr(8);
scheme = Aws::Http::Scheme::HTTPS;
}

return {normalized, scheme};
}

StatusCode S3FileSystem::parsePath(const std::string& path, std::string* bucket, std::string* object) {
std::smatch sm;

Expand Down Expand Up @@ -127,12 +142,9 @@ S3FileSystem::S3FileSystem(const Aws::SDKOptions& options, const std::string& s3
config.scheme = Aws::Http::Scheme::HTTP;
}
if (s3_endpoint != nullptr) {
std::string endpoint(s3_endpoint);
if (endpoint.rfind("http://") != std::string::npos) {
endpoint = endpoint.substr(7);
}
config.endpointOverride = Aws::String(endpoint.c_str());
config.scheme = Aws::Http::Scheme::HTTP;
auto parsed = S3FileSystem::parseEndpoint(s3_endpoint);
config.scheme = parsed.second;
config.endpointOverride = Aws::String(parsed.first.c_str());
}

if (!default_proxy.empty()) {
Expand Down
9 changes: 9 additions & 0 deletions src/filesystem/s3filesystem.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

#include <regex>
#include <string>
#include <utility>
#include <vector>

#include <aws/core/Aws.h>
Expand All @@ -29,6 +30,14 @@ namespace ovms {

class S3FileSystem : public FileSystem {
public:
/**
* @brief Resolve an S3-compatible endpoint into a clean host:port string and scheme.
*
* If no scheme is provided, HTTP is used by default for compatibility with MinIO
* and other S3-compatible deployments that expose plain HTTP endpoints.
*/
static std::pair<std::string, Aws::Http::Scheme> parseEndpoint(const std::string& endpoint);

/**
* @brief Construct a new S3FileSystem object
*
Expand Down
33 changes: 28 additions & 5 deletions src/kfs_frontend/kfs_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "kfs_utils.hpp"

#include <algorithm>
#include <cstring>
#include <limits>
#include <map>
#include <memory>
Expand Down Expand Up @@ -323,11 +324,28 @@ Status buildShapeFromStringTensorRequest(const ::KFSRequest::InferInputTensor& s

Status convertBinaryExtensionStringFromBufferToNativeOVTensor(const ::KFSRequest::InferInputTensor& src, ov::Tensor& tensor, const std::string* buffer) {
std::vector<uint32_t> stringSizes;
uint32_t totalStringsLength = 0;
while (totalStringsLength + stringSizes.size() * sizeof(uint32_t) + sizeof(uint32_t) <= buffer->size()) {
uint32_t inputSize = *(reinterpret_cast<const uint32_t*>(buffer->data() + totalStringsLength + stringSizes.size() * sizeof(uint32_t)));
size_t totalStringsLength = 0;
while (true) {
const size_t headersLength = stringSizes.size() * sizeof(uint32_t);
if (headersLength > buffer->size()) {
break;
}
if (totalStringsLength > buffer->size() - headersLength) {
break;
}
const size_t currentOffset = totalStringsLength + headersLength;
if (buffer->size() - currentOffset < sizeof(uint32_t)) {
break;
}
Comment on lines +330 to +339

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we simply break from those conditions? Not return error?

uint32_t inputSize = 0;
std::memcpy(&inputSize, buffer->data() + currentOffset, sizeof(inputSize));
const size_t remainingStringBytes = buffer->size() - currentOffset - sizeof(uint32_t);
if (static_cast<size_t>(inputSize) > remainingStringBytes) {
SPDLOG_DEBUG("Input string format conversion failed");
return StatusCode::INVALID_STRING_INPUT;
}
stringSizes.push_back(inputSize);
totalStringsLength += inputSize;
totalStringsLength += static_cast<size_t>(inputSize);
}
size_t batchSize = stringSizes.size();
if ((totalStringsLength + batchSize * sizeof(uint32_t)) != buffer->size()) {
Expand All @@ -343,7 +361,12 @@ Status convertBinaryExtensionStringFromBufferToNativeOVTensor(const ::KFSRequest
std::string* data = tensor.data<std::string>();
size_t tensorStringsOffset = 0;
for (size_t i = 0; i < stringSizes.size(); i++) {
data[i].assign(reinterpret_cast<const char*>(buffer->data() + (i + 1) * sizeof(uint32_t) + tensorStringsOffset), stringSizes[i]);
const size_t sourceOffset = (i + 1) * sizeof(uint32_t) + tensorStringsOffset;
if (sourceOffset > buffer->size() || static_cast<size_t>(stringSizes[i]) > buffer->size() - sourceOffset) {
SPDLOG_DEBUG("Input string format conversion failed");
return StatusCode::INVALID_STRING_INPUT;
}
data[i].assign(reinterpret_cast<const char*>(buffer->data() + sourceOffset), stringSizes[i]);
tensorStringsOffset += stringSizes[i];
}
return StatusCode::OK;
Expand Down
17 changes: 14 additions & 3 deletions src/tensor_conversion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,24 @@ shape_t getShapeFromImages(const std::vector<cv::Mat>& images, const TensorInfo&

ov::Tensor createTensorFromMats(const std::vector<cv::Mat>& images, const TensorInfo& tensorInfo) {
OVMS_PROFILE_FUNCTION();
if (images.empty()) {
return ov::Tensor();
}
ov::Shape shape = getShapeFromImages(images, tensorInfo);
ov::element::Type precision = tensorInfo.getOvPrecision();
ov::Tensor tensor(precision, shape);
char* ptr = (char*)tensor.data();
for (cv::Mat image : images) {
memcpy(ptr, (char*)image.data, image.total() * image.elemSize());
ptr += (image.total() * image.elemSize());
const size_t firstImageSizeBytes = images[0].total() * images[0].elemSize();
for (const auto& image : images) {
const size_t imageSizeBytes = image.total() * image.elemSize();
if (imageSizeBytes != firstImageSizeBytes) {
SPDLOG_DEBUG("Image conversion failed due to inconsistent image size in one batch. Expected bytes: {} current bytes: {}",
firstImageSizeBytes,
imageSizeBytes);
return ov::Tensor();
}
memcpy(ptr, (char*)image.data, imageSizeBytes);
ptr += imageSizeBytes;
}
return tensor;
}
Expand Down
8 changes: 8 additions & 0 deletions src/tensor_conversion.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ static Status convertTensorToMatsMatchingTensorInfo(const TensorType& src, std::
image = std::move(imageResized);
}

if (firstImage != nullptr && image.channels() != firstImage->channels()) {
SPDLOG_DEBUG("Binary data sent to input: {} has mixed channel count within one batch. First image channels: {} current image channels: {}",
tensorInfo.getMappedName(),
firstImage->channels(),
image.channels());
return StatusCode::INVALID_NO_OF_CHANNELS;
}

// if (i == 0 && src.contents().bytes_contents_size() > 1) {
// // Multiply src.string_val_size() * image resolution * precision size
// }
Expand Down
17 changes: 14 additions & 3 deletions src/tensor_conversion_common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -236,13 +236,24 @@ shape_t getShapeFromImages(const std::vector<cv::Mat>& images, const TensorInfo&
}
ov::Tensor createTensorFromMats(const std::vector<cv::Mat>& images, const TensorInfo& tensorInfo) {
OVMS_PROFILE_FUNCTION();
if (images.empty()) {
return ov::Tensor();
}
ov::Shape shape = getShapeFromImages(images, tensorInfo);
ov::element::Type precision = tensorInfo.getOvPrecision();
ov::Tensor tensor(precision, shape);
char* ptr = (char*)tensor.data();
for (cv::Mat image : images) {
memcpy(ptr, (char*)image.data, image.total() * image.elemSize());
ptr += (image.total() * image.elemSize());
const size_t firstImageSizeBytes = images[0].total() * images[0].elemSize();
for (const auto& image : images) {
const size_t imageSizeBytes = image.total() * image.elemSize();
if (imageSizeBytes != firstImageSizeBytes) {
SPDLOG_DEBUG("Image conversion failed due to inconsistent image size in one batch. Expected bytes: {} current bytes: {}",
firstImageSizeBytes,
imageSizeBytes);
return ov::Tensor();
}
memcpy(ptr, (char*)image.data, imageSizeBytes);
ptr += imageSizeBytes;
}
return tensor;
}
Expand Down
15 changes: 15 additions & 0 deletions src/test/localfilesystem_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@

#include "src/filesystem/filesystem.hpp"
#include "src/filesystem/localfilesystem.hpp"
#if CLOUD_DISABLE == 0
#include "src/filesystem/s3filesystem.hpp"
#endif

using namespace testing;
using ::testing::UnorderedElementsAre;
Expand Down Expand Up @@ -54,6 +57,18 @@ static void createTmpFiles() {
std::filesystem::create_directories(TMP_PATH / TMP_DIR2);
}

#if CLOUD_DISABLE == 0
TEST(S3FileSystem, ParseEndpointUsesHttpByDefault) {
auto http_default = ovms::S3FileSystem::parseEndpoint("localhost:9000");
EXPECT_EQ(http_default.first, "localhost:9000");
EXPECT_EQ(http_default.second, Aws::Http::Scheme::HTTP);

auto https_explicit = ovms::S3FileSystem::parseEndpoint("https://localhost:9000");
EXPECT_EQ(https_explicit.first, "localhost:9000");
EXPECT_EQ(https_explicit.second, Aws::Http::Scheme::HTTPS);
}
#endif

TEST(LocalFileSystem, FileExists) {
ovms::LocalFileSystem lfs;
bool exists = false;
Expand Down
79 changes: 79 additions & 0 deletions src/test/node_library_manager_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>

#include <array>
#include <vector>

#include "../dags/custom_node_library_manager.hpp"
#include "constructor_enabled_model_manager.hpp"
#include "platform_utils.hpp"
Expand Down Expand Up @@ -109,6 +112,82 @@ TEST(NodeLibraryManagerTest, ErrorWhenLibraryPathNotEscaped) {
EXPECT_EQ(status, StatusCode::PATH_INVALID);
}

TEST(NodeLibraryManagerTest, ModelZooObjectDetectionCapsAllOutputsToMaxOutputBatch) {
CustomNodeLibraryManager manager;
NodeLibrary library;
auto status = manager.loadLibrary("model_zoo_object_detection", getGenericFullPathForBazelOut("/ovms/bazel-bin/src/libcustom_node_model_zoo_intel_object_detection.so"));
ASSERT_EQ(status, StatusCode::OK);
ASSERT_EQ(manager.getLibrary("model_zoo_object_detection", library), StatusCode::OK);

std::array<CustomNodeParam, 7> params = {{{"original_image_height", "4"},
{"original_image_width", "4"},
{"target_image_height", "2"},
{"target_image_width", "2"},
{"confidence_threshold", "0.5"},
{"max_output_batch", "2"},
{"buffer_queue_size", "2"}}};

void* customNodeLibraryInternalManager = nullptr;
ASSERT_EQ(library.initialize(&customNodeLibraryInternalManager, params.data(), params.size()), 0);

std::vector<float> imageData(1 * 3 * 4 * 4, 1.0f);
std::vector<uint64_t> imageDims{1, 3, 4, 4};
CustomNodeTensor imageTensor{
"image",
reinterpret_cast<uint8_t*>(imageData.data()),
static_cast<uint64_t>(imageData.size() * sizeof(float)),
imageDims.data(),
imageDims.size(),
FP32};

const uint64_t detectionsCount = 5;
const uint64_t featuresCount = 7;
std::vector<float> detectionData(detectionsCount * featuresCount, 0.0f);
for (size_t i = 0; i < detectionsCount; ++i) {
auto* detection = detectionData.data() + i * featuresCount;
detection[0] = 0.0f; // image_id
detection[1] = 1.0f; // label_id
detection[2] = 0.99f; // confidence
detection[3] = 0.1f;
detection[4] = 0.1f;
detection[5] = 0.9f;
detection[6] = 0.9f;
}
std::vector<uint64_t> detectionDims{1, 1, detectionsCount, featuresCount};
CustomNodeTensor detectionTensor{
"detection",
reinterpret_cast<uint8_t*>(detectionData.data()),
static_cast<uint64_t>(detectionData.size() * sizeof(float)),
detectionDims.data(),
detectionDims.size(),
FP32};

std::array<CustomNodeTensor, 2> inputs{imageTensor, detectionTensor};
CustomNodeTensor* outputs = nullptr;
int outputsCount = 0;

ASSERT_EQ(library.execute(inputs.data(), inputs.size(), &outputs, &outputsCount, params.data(), params.size(), customNodeLibraryInternalManager), 0);
ASSERT_NE(outputs, nullptr);
ASSERT_EQ(outputsCount, 4);

constexpr uint64_t maxOutputBatch = 2;
for (int i = 0; i < outputsCount; ++i) {
ASSERT_NE(outputs[i].dims, nullptr);
ASSERT_GT(outputs[i].dimsCount, 0);
EXPECT_EQ(outputs[i].dims[0], maxOutputBatch);
}
EXPECT_EQ(outputs[1].dataBytes, sizeof(int32_t) * 4 * maxOutputBatch); // coordinates
EXPECT_EQ(outputs[2].dataBytes, sizeof(float) * maxOutputBatch); // confidences
EXPECT_EQ(outputs[3].dataBytes, sizeof(int32_t) * maxOutputBatch); // label_ids

for (int i = 0; i < outputsCount; ++i) {
library.release(outputs[i].data, customNodeLibraryInternalManager);
library.release(outputs[i].dims, customNodeLibraryInternalManager);
}
library.release(outputs, customNodeLibraryInternalManager);
EXPECT_EQ(library.deinitialize(customNodeLibraryInternalManager), 0);
}

class ModelManagerNodeLibraryTest : public TestWithTempDir {};

TEST_F(ModelManagerNodeLibraryTest, LoadCustomNodeLibrary) {
Expand Down
Loading