Skip to content
Merged
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
1 change: 1 addition & 0 deletions src/image_gen/imagegenpipelineargs.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ struct ImageGenPipelineArgs {
ov::AnyMap pluginConfig;
resolution_t maxResolution;
std::optional<resolution_t> defaultResolution;
std::optional<resolution_t> nativeResolution;
std::optional<uint64_t> seed;
uint64_t maxNumImagesPerPrompt;
uint64_t defaultNumInferenceSteps;
Expand Down
69 changes: 45 additions & 24 deletions src/image_gen/imagegenutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ static std::variant<absl::Status, std::vector<std::string>> convert2Strings(cons
} catch (...) {
return absl::InternalError("Unknown error during image conversion");
}
return absl::OkStatus();
}

std::variant<absl::Status, std::optional<resolution_t>> getDimensions(const std::string& dimensions) {
Expand Down Expand Up @@ -259,18 +258,30 @@ absl::Status ensureAcceptableForStatic(const ov::AnyMap& requestOptions, const o
return absl::OkStatus();
}

absl::Status ensureAcceptableAndDefaultsSetRequestOptions(ov::AnyMap& requestOptions, const ovms::ImageGenPipelineArgs& args) {
// validate for static
if (args.staticReshapeSettings.has_value()) {
SPDLOG_DEBUG("Validating request options for static reshape settings");
auto status = ensureAcceptableForStatic(requestOptions, args);
if (!status.ok()) {
return status;
absl::Status ensureCommonValidations(ov::AnyMap& requestOptions, const ovms::ImageGenPipelineArgs& args) {
// Basic sanity checks that apply to both static and dynamic modes
auto it = requestOptions.find("width");
if (it != requestOptions.end()) {
auto width = it->second.as<int64_t>();
if (width <= 0) {
return absl::InvalidArgumentError(absl::StrCat("requested width must be higher than 0, but got ", width));
}
if (width > args.maxResolution.first) {
return absl::InvalidArgumentError(absl::StrCat("requested width ", width, " exceeds maxResolution width: ", args.maxResolution.first));
}
}
it = requestOptions.find("height");
if (it != requestOptions.end()) {
auto height = it->second.as<int>();
if (height <= 0) {
return absl::InvalidArgumentError(absl::StrCat("requested height must be higher than 0, but got ", height));
}
if (height > args.maxResolution.second) {
return absl::InvalidArgumentError(absl::StrCat("requested height ", height, " exceeds maxResolution height: ", args.maxResolution.second));
}
}
Comment thread
kbalka marked this conversation as resolved.

// check if we have any unhandled parameters
auto it = requestOptions.find("num_images_per_prompt");
it = requestOptions.find("num_images_per_prompt");
if (it != requestOptions.end()) {
auto numImages = it->second.as<int>();
if (numImages > args.maxNumImagesPerPrompt) {
Expand All @@ -280,6 +291,7 @@ absl::Status ensureAcceptableAndDefaultsSetRequestOptions(ov::AnyMap& requestOpt
return absl::InvalidArgumentError(absl::StrCat("num_images_per_prompt/n must be higher than 0"));
}
}

it = requestOptions.find("num_inference_steps");
if (it != requestOptions.end()) {
auto numInferenceSteps = it->second.as<int>();
Expand All @@ -292,20 +304,7 @@ absl::Status ensureAcceptableAndDefaultsSetRequestOptions(ov::AnyMap& requestOpt
} else {
requestOptions.insert({"num_inference_steps", args.defaultNumInferenceSteps});
}
it = requestOptions.find("width");
if (it != requestOptions.end()) {
auto width = it->second.as<int>();
if (width <= 0) {
return absl::InvalidArgumentError(absl::StrCat("width must be higher than 0"));
}
}
it = requestOptions.find("height");
if (it != requestOptions.end()) {
auto height = it->second.as<int>();
if (height <= 0) {
return absl::InvalidArgumentError(absl::StrCat("height must be higher than 0"));
}
}

it = requestOptions.find("strength");
if (it != requestOptions.end()) {
auto strength = it->second.as<float>();
Expand All @@ -318,6 +317,20 @@ absl::Status ensureAcceptableAndDefaultsSetRequestOptions(ov::AnyMap& requestOpt
return absl::OkStatus();
}

absl::Status ensureAcceptableAndDefaultsSetRequestOptions(ov::AnyMap& requestOptions, const ovms::ImageGenPipelineArgs& args) {
// Apply common validations first (basic sanity checks)
auto status = ensureCommonValidations(requestOptions, args);
if (!status.ok()) {
return status;
}
// validate for static
if (args.staticReshapeSettings.has_value()) {
SPDLOG_DEBUG("Validating request options for static reshape settings");
return ensureAcceptableForStatic(requestOptions, args);
}
return absl::OkStatus();
}

std::variant<absl::Status, ov::AnyMap> getImageGenerationRequestOptions(const rapidjson::Document& parser, const ovms::ImageGenPipelineArgs& args, bool hasDynamicAdapters) {
// NO -not handled yet
// OpenAI parameters
Expand Down Expand Up @@ -403,6 +416,14 @@ std::variant<absl::Status, ov::AnyMap> getImageGenerationRequestOptions(const ra
requestOptions.insert({"width", args.defaultResolution->first});
}
}
if (args.nativeResolution.has_value()) {
if (requestOptions.find("height") == requestOptions.end()) {
requestOptions.insert({"height", args.nativeResolution->second});
}
if (requestOptions.find("width") == requestOptions.end()) {
requestOptions.insert({"width", args.nativeResolution->first});
}
}
// now check if in httpPaylod.parsedJson we have any fields other than the ones we accept
// accepted are: prompt, prompt_2, prompt_3, negative_prompt, negative_prompt_2, negative_prompt_3,
// num_images_per_prompt, max_sequence_length, height, width, rng_seed, num_inference_steps,
Expand Down
13 changes: 13 additions & 0 deletions src/image_gen/pipelines.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,19 @@ ImageGenerationPipelines::ImageGenerationPipelines(const ImageGenPipelineArgs& a
throw std::runtime_error("Failed to create any image generation pipeline from: " + args.modelsPath);
}

if (text2ImagePipeline && !args.defaultResolution.has_value()) {
const auto nativeConfig = text2ImagePipeline->get_generation_config();
if (nativeConfig.width <= 0 || nativeConfig.height <= 0) {
throw std::runtime_error("Failed to determine model-native image generation resolution");
}
this->args.nativeResolution = resolution_t{nativeConfig.width, nativeConfig.height};
Comment thread
kbalka marked this conversation as resolved.
if (nativeConfig.width > args.maxResolution.first || nativeConfig.height > args.maxResolution.second) {
SPDLOG_WARN("Model-native image generation resolution ({}x{}) exceeds maxResolution ({}x{}); "
"requests using the native default resolution will be rejected",
nativeConfig.width, nativeConfig.height, args.maxResolution.first, args.maxResolution.second);
}
}

// InpaintingPipeline does not support clone(), so concurrent inpainting
// requests must be serialized
if (inpaintingPipeline) {
Expand Down
158 changes: 152 additions & 6 deletions src/test/text2image_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ ovms::ImageGenPipelineArgs DEFAULTIMAGE_GEN_ARGS{
ov::AnyMap(),
{4096, 4096}, // maxResolution
std::nullopt, // defaultResolution
std::nullopt, // nativeResolution
std::nullopt, // seed
10, // maxNumImagesPerPrompt
10, // defaultNumInferenceSteps
Expand Down Expand Up @@ -137,6 +138,48 @@ void testNegativeDimensions(const std::string& dims) {
ASSERT_TRUE(std::holds_alternative<absl::Status>(dimensions)) << dims;
EXPECT_EQ(std::get<absl::Status>(dimensions).code(), absl::StatusCode::kInvalidArgument) << dims;
}

void testMaxResolutionExceeded(std::optional<std::pair<unsigned int, unsigned int>> maxRes, const std::string& dims) {
// /create JSON
ovms::HttpPayload payload;
payload.parsedJson = std::make_shared<rapidjson::Document>();
payload.parsedJson->Parse(dims.c_str());
ovms::ImageGenPipelineArgs args = DEFAULTIMAGE_GEN_ARGS;
args.maxResolution = maxRes.value_or(args.maxResolution);
std::variant<absl::Status, ov::AnyMap> requestOptions = ovms::getImageGenerationRequestOptions(*payload.parsedJson, args, true);

ASSERT_TRUE(std::holds_alternative<absl::Status>(requestOptions)) << dims;

EXPECT_EQ(std::get<absl::Status>(requestOptions).code(), absl::StatusCode::kInvalidArgument) << dims;
}

void testMaxResolutionNotExceeded(std::optional<std::pair<unsigned int, unsigned int>> maxRes, const std::string& dims) {
// /create JSON
ovms::HttpPayload payload;
payload.parsedJson = std::make_shared<rapidjson::Document>();
payload.parsedJson->Parse(dims.c_str());
ovms::ImageGenPipelineArgs args = DEFAULTIMAGE_GEN_ARGS;
args.maxResolution = maxRes.value_or(args.maxResolution);
std::variant<absl::Status, ov::AnyMap> requestOptions = ovms::getImageGenerationRequestOptions(*payload.parsedJson, args, true);

ASSERT_TRUE(std::holds_alternative<ov::AnyMap>(requestOptions)) << dims;
}

TEST(Text2ImageTest, testMaxResolutionExceeded) {
testMaxResolutionExceeded(std::make_optional(std::make_pair(500, 500)), R"({"size":"1000x1000"})");
testMaxResolutionExceeded(std::make_optional(std::make_pair(500, 500)), R"({"size":"400x1000"})");
testMaxResolutionExceeded(std::make_optional(std::make_pair(500, 500)), R"({"size":"1000x400"})");
testMaxResolutionExceeded(std::nullopt, R"({"size":"5000x5000"})");
}

TEST(Text2ImageTest, testMaxResolutionNotExceeded) {
testMaxResolutionNotExceeded(std::nullopt, R"({"size":"1000x1000"})");
testMaxResolutionNotExceeded(std::make_optional(std::make_pair(2000, 2000)), R"({"size":"1000x500"})");
testMaxResolutionNotExceeded(std::make_optional(std::make_pair(2000, 2000)), R"({"size":"500x1000"})");
testMaxResolutionNotExceeded(std::make_optional(std::make_pair(2000, 2000)), R"({"size":"1000x1000"})");
}


TEST(Text2ImageTest, testGetDimensionsNegativeImproperFormat) {
testNegativeDimensions(R"({"size":"51:512"})");
testNegativeDimensions(R"({"size":"512_51x"})");
Expand All @@ -147,6 +190,8 @@ TEST(Text2ImageTest, testGetDimensionsNegativeImproperFormat) {
testNegativeDimensions(R"({"size":"51x0"})");
testNegativeDimensions(R"({"size":"abcx512"})");
testNegativeDimensions(R"({"size":"5151xabc"})");
testNegativeDimensions(R"({"size":"5151x"})");
testNegativeDimensions(R"({"size":"x5151"})");
// max int64_t 9223372036854775807
// min int64_t -9223372036854775808
testNegativeDimensions(R"({"size":"9223372036854775808x1"})");
Expand Down Expand Up @@ -467,7 +512,8 @@ TEST(Text2ImageTest, getImageGenerationRequestOptionsWidth0) {
auto requestOptions = ovms::getImageGenerationRequestOptions(*payload.parsedJson, DEFAULTIMAGE_GEN_ARGS);
ASSERT_TRUE(std::holds_alternative<absl::Status>(requestOptions));
auto& err = std::get<absl::Status>(requestOptions);
EXPECT_EQ(err.message(), "width must be higher than 0");

EXPECT_THAT(err.message(), ::testing::HasSubstr("width must be higher than 0"));
}
TEST(Text2ImageTest, getImageGenerationRequestOptionsHeight0) {
ovms::HttpPayload payload;
Expand All @@ -484,7 +530,7 @@ TEST(Text2ImageTest, getImageGenerationRequestOptionsHeight0) {
auto requestOptions = ovms::getImageGenerationRequestOptions(*payload.parsedJson, DEFAULTIMAGE_GEN_ARGS);
ASSERT_TRUE(std::holds_alternative<absl::Status>(requestOptions));
auto& err = std::get<absl::Status>(requestOptions);
EXPECT_EQ(err.message(), "height must be higher than 0");
EXPECT_THAT(err.message(), ::testing::HasSubstr("height must be higher than 0"));
}
TEST(Text2ImageTest, getImageGenerationRequestOptionsN0) {
ovms::HttpPayload payload;
Expand Down Expand Up @@ -630,6 +676,54 @@ TEST(Text2ImageTest, getImageGenerationRequestOptionsDefaultSizeBehavior) {
EXPECT_EQ(options.at("height").as<int64_t>(), 256);
EXPECT_EQ(options.at("width").as<int64_t>(), 512);
}

TEST(Text2ImageTest, getImageGenerationRequestOptionsUsesNativeResolutionWhenSizeIsMissing) {
ovms::HttpPayload payload;
payload.parsedJson = std::make_shared<rapidjson::Document>();
payload.parsedJson->Parse(R"({"prompt":"test prompt","size":"auto"})");
auto args = DEFAULTIMAGE_GEN_ARGS;
args.nativeResolution = std::make_pair(1024, 768);

auto requestOptions = ovms::getImageGenerationRequestOptions(*payload.parsedJson, args);
ASSERT_TRUE(std::holds_alternative<ov::AnyMap>(requestOptions));
auto& options = std::get<ov::AnyMap>(requestOptions);
EXPECT_EQ(options.at("width").as<int64_t>(), 1024);
EXPECT_EQ(options.at("height").as<int64_t>(), 768);

payload.parsedJson->Parse(R"({"prompt":"test prompt"})");
requestOptions = ovms::getImageGenerationRequestOptions(*payload.parsedJson, args);
ASSERT_TRUE(std::holds_alternative<ov::AnyMap>(requestOptions));
auto& optionsWithoutSize = std::get<ov::AnyMap>(requestOptions);
EXPECT_EQ(optionsWithoutSize.at("width").as<int64_t>(), 1024);
EXPECT_EQ(optionsWithoutSize.at("height").as<int64_t>(), 768);
}

TEST(Text2ImageTest, getImageGenerationRequestOptionsRejectsNativeResolutionAboveMax) {
ovms::HttpPayload payload;
payload.parsedJson = std::make_shared<rapidjson::Document>();
payload.parsedJson->Parse(R"({"prompt":"test prompt","size":"auto"})");
auto args = DEFAULTIMAGE_GEN_ARGS;
args.maxResolution = {64, 64};
args.nativeResolution = std::make_pair(128, 64);

auto requestOptions = ovms::getImageGenerationRequestOptions(*payload.parsedJson, args);
ASSERT_TRUE(std::holds_alternative<absl::Status>(requestOptions));
EXPECT_THAT(std::get<absl::Status>(requestOptions).message(), ::testing::HasSubstr("width 128 exceeds maxResolution"));
}

TEST(Text2ImageTest, getImageGenerationRequestOptionsRejectsNativeResolutionAboveMaxWhenSizeIsMissing) {
ovms::HttpPayload payload;
payload.parsedJson = std::make_shared<rapidjson::Document>();
payload.parsedJson->Parse(R"({"prompt":"test prompt"})");
auto args = DEFAULTIMAGE_GEN_ARGS;
args.maxResolution = {64, 64};
args.nativeResolution = std::make_pair(128, 64);

auto requestOptions = ovms::getImageGenerationRequestOptions(*payload.parsedJson, args);
ASSERT_TRUE(std::holds_alternative<absl::Status>(requestOptions));
EXPECT_THAT(std::get<absl::Status>(requestOptions).message(), ::testing::HasSubstr("width 128 exceeds maxResolution"));
}

TEST(Text2ImageTest, getImageGenerationRequestOptionsRejectedFields) {
// OpenAI fields background, mask, quality, response_format, user
ovms::HttpPayload payload;
Expand Down Expand Up @@ -1411,12 +1505,13 @@ TEST(Text2ImageTest, getImageGenerationRequestOptionsValidatedFields) {
}
}
TEST(Text2ImageTest, validateForStaticReshapeSettings_MatchesOneResolution) {
ImageGenPipelineArgs args;
ImageGenPipelineArgs args = DEFAULTIMAGE_GEN_ARGS;
args.modelsPath = "/ovms/src/test/dummy";
args.device.push_back("NPU");
args.defaultNumInferenceSteps = 10;
args.maxNumInferenceSteps = 50;
args.maxNumImagesPerPrompt = 10;

args.staticReshapeSettings = ovms::StaticReshapeSettingsArgs({{512, 256}, {1024, 512}, {2048, 1024}});

std::string value = R"({"prompt": "test prompt", "size": "1024x512", "n": 1, "model": "test model"})";
Expand All @@ -1427,10 +1522,61 @@ TEST(Text2ImageTest, validateForStaticReshapeSettings_MatchesOneResolution) {
auto requestOptions = ovms::getImageGenerationRequestOptions(*payload.parsedJson, args);
bool holdsStatus = std::holds_alternative<absl::Status>(requestOptions);
ASSERT_FALSE(holdsStatus) << std::get<absl::Status>(requestOptions).ToString();
auto& options = std::get<ov::AnyMap>(requestOptions);
EXPECT_EQ(options.at("num_inference_steps").as<uint64_t>(), args.defaultNumInferenceSteps);
}

TEST(Text2ImageTest, validateForStaticReshapeSettings_ValidatesInferenceSteps) {
ImageGenPipelineArgs args = DEFAULTIMAGE_GEN_ARGS;
args.modelsPath = "/ovms/src/test/dummy";
args.maxNumInferenceSteps = 50;
args.staticReshapeSettings = ovms::StaticReshapeSettingsArgs({{512, 256}});

ovms::HttpPayload payload;
payload.parsedJson = std::make_shared<rapidjson::Document>();
payload.parsedJson->Parse(R"({"prompt":"test prompt","size":"512x256","n":1,"num_inference_steps":0})");
auto requestOptions = ovms::getImageGenerationRequestOptions(*payload.parsedJson, args);
ASSERT_TRUE(std::holds_alternative<absl::Status>(requestOptions));
EXPECT_THAT(std::get<absl::Status>(requestOptions).message(), ::testing::HasSubstr("num_inference_steps must be higher than 0"));

payload.parsedJson->Parse(R"({"prompt":"test prompt","size":"512x256","n":1,"num_inference_steps":51})");
requestOptions = ovms::getImageGenerationRequestOptions(*payload.parsedJson, args);
ASSERT_TRUE(std::holds_alternative<absl::Status>(requestOptions));
EXPECT_THAT(std::get<absl::Status>(requestOptions).message(), ::testing::HasSubstr("num_inference_steps is greater than maxNumInferenceSteps"));
}

TEST(Text2ImageTest, validateForStaticReshapeSettings_ValidatesMaxNumImagesPerPrompt) {
ImageGenPipelineArgs args = DEFAULTIMAGE_GEN_ARGS;
args.modelsPath = "/ovms/src/test/dummy";
args.maxNumImagesPerPrompt = 1;
args.staticReshapeSettings = ovms::StaticReshapeSettingsArgs({{512, 256}, {1024, 512}});

ovms::HttpPayload payload;
payload.parsedJson = std::make_shared<rapidjson::Document>();
payload.parsedJson->Parse(R"({"prompt":"test prompt","size":"1024x512","n":2})");
auto requestOptions = ovms::getImageGenerationRequestOptions(*payload.parsedJson, args);
ASSERT_TRUE(std::holds_alternative<absl::Status>(requestOptions));
EXPECT_THAT(std::get<absl::Status>(requestOptions).message(), ::testing::HasSubstr("num_images_per_prompt/n is greater than maxNumImagesPerPrompt"));
}

TEST(Text2ImageTest, validateForStaticReshapeSettings_ExceedsDefaultMaxResolution) {
ImageGenPipelineArgs args = DEFAULTIMAGE_GEN_ARGS;
args.modelsPath = "/ovms/src/test/dummy";
args.staticReshapeSettings = ovms::StaticReshapeSettingsArgs({{5000, 512}});

std::string value = R"({"prompt": "test prompt", "size": "5000x512", "n": 1, "model": "test model"})";
ovms::HttpPayload payload;
payload.parsedJson = std::make_shared<rapidjson::Document>();
payload.parsedJson->Parse(value.c_str());
ASSERT_FALSE(payload.parsedJson->HasParseError());

auto requestOptions = ovms::getImageGenerationRequestOptions(*payload.parsedJson, args);
ASSERT_TRUE(std::holds_alternative<absl::Status>(requestOptions));
EXPECT_THAT(std::get<absl::Status>(requestOptions).message(), ::testing::HasSubstr("exceeds maxResolution"));
}
Comment thread
kbalka marked this conversation as resolved.

TEST(Text2ImageTest, validateForStaticReshapeSettings_DoesntMatchResolution) {
ImageGenPipelineArgs args;
ImageGenPipelineArgs args = DEFAULTIMAGE_GEN_ARGS;
args.modelsPath = "/ovms/src/test/dummy";
args.device.push_back("NPU");
args.defaultNumInferenceSteps = 10;
Expand All @@ -1452,7 +1598,7 @@ TEST(Text2ImageTest, validateForStaticReshapeSettings_DoesntMatchResolution) {
}

TEST(Text2ImageTest, validateForStaticReshapeSettings_NegativeStatic4ButRequested5NumImagesPerPrompt) {
ImageGenPipelineArgs args;
ImageGenPipelineArgs args = DEFAULTIMAGE_GEN_ARGS;
args.modelsPath = "/ovms/src/test/dummy";
args.device.push_back("NPU");
args.defaultNumInferenceSteps = 10;
Expand All @@ -1475,7 +1621,7 @@ TEST(Text2ImageTest, validateForStaticReshapeSettings_NegativeStatic4ButRequeste


TEST(Text2ImageTest, validateForStaticReshapeSettings_DoesntMatchGuidanceScale) {
ImageGenPipelineArgs args;
ImageGenPipelineArgs args = DEFAULTIMAGE_GEN_ARGS;
args.modelsPath = "/ovms/src/test/dummy";
args.device.push_back("NPU");
args.defaultNumInferenceSteps = 10;
Expand Down