Skip to content

Commit 81c5e50

Browse files
authored
Image generation multiconcurrency (#2190) (#2284)
Cherry pick from release branch: #2190
1 parent bd0a2d0 commit 81c5e50

34 files changed

+548
-7
lines changed

samples/cpp/image_generation/CMakeLists.txt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,22 @@ install(TARGETS text2image
3838
COMPONENT samples_bin
3939
EXCLUDE_FROM_ALL)
4040

41+
# create text2image concurrent sample executable
42+
43+
add_executable(text2image_concurrency text2image_concurrency.cpp imwrite.cpp)
44+
45+
target_include_directories(text2image_concurrency PRIVATE ${CMAKE_BINARY_DIR} "${CMAKE_CURRENT_SOURCE_DIR}")
46+
target_link_libraries(text2image_concurrency PRIVATE openvino::genai indicators::indicators)
47+
48+
set_target_properties(text2image_concurrency PROPERTIES
49+
# Ensure out of box LC_RPATH on macOS with SIP
50+
INSTALL_RPATH_USE_LINK_PATH ON)
51+
52+
install(TARGETS text2image_concurrency
53+
RUNTIME DESTINATION samples_bin/
54+
COMPONENT samples_bin
55+
EXCLUDE_FROM_ALL)
56+
4157
# create LoRA sample executable
4258

4359
add_executable(lora_text2image lora_text2image.cpp imwrite.cpp)
@@ -88,6 +104,22 @@ install(TARGETS image2image
88104
COMPONENT samples_bin
89105
EXCLUDE_FROM_ALL)
90106

107+
# create image2image concurrent sample executable
108+
109+
add_executable(image2image_concurrency image2image_concurrency.cpp load_image.cpp imwrite.cpp)
110+
111+
target_include_directories(image2image_concurrency PRIVATE ${CMAKE_BINARY_DIR} "${CMAKE_CURRENT_SOURCE_DIR}")
112+
target_link_libraries(image2image_concurrency PRIVATE openvino::genai indicators::indicators)
113+
114+
set_target_properties(image2image_concurrency PROPERTIES
115+
# Ensure out of box LC_RPATH on macOS with SIP
116+
INSTALL_RPATH_USE_LINK_PATH ON)
117+
118+
install(TARGETS image2image_concurrency
119+
RUNTIME DESTINATION samples_bin/
120+
COMPONENT samples_bin
121+
EXCLUDE_FROM_ALL)
122+
91123
# create inpainting executable
92124

93125
add_executable(inpainting inpainting.cpp load_image.cpp imwrite.cpp)

samples/cpp/image_generation/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ Examples in this folder showcase inference of text to image models like Stable D
44

55
There are several sample files:
66
- [`text2image.cpp`](./text2image.cpp) demonstrates basic usage of the text to image pipeline
7+
- [`text2image_concurrency.cpp`](./text2image_concurrency.cpp) demonstrates concurrent usage of the text to image pipeline to create multiple images with different prompts
78
- [`lora_text2image.cpp`](./lora_text2image.cpp) shows how to apply LoRA adapters to the pipeline
89
- [`heterogeneous_stable_diffusion.cpp`](./heterogeneous_stable_diffusion.cpp) shows how to assemble a heterogeneous txt2image pipeline from individual subcomponents (scheduler, text encoder, unet, vae decoder)
910
- [`image2image.cpp`](./image2image.cpp) demonstrates basic usage of the image to image pipeline
11+
- [`image2image_concurrency.cpp.cpp`](./image2image_concurrency.cpp) demonstrates concurrent usage of the image to image pipeline to create multiple images with different prompts
1012
- [`inpainting.cpp`](./inpainting.cpp) demonstrates basic usage of the inpainting pipeline
1113
- [`benchmark_image_gen.cpp`](./benchmark_image_gen.cpp) demonstrates how to benchmark the text to image / image to image / inpainting pipeline
1214

@@ -210,3 +212,42 @@ Test finish, load time: 9356.00 ms
210212
Warmup number:1, first generate warmup time:85008.00 ms, infer warmup time:84999.88 ms
211213
Generate iteration number:3, for one iteration, generate avg time: 84372.34 ms, infer avg time:84363.95 ms, all text encoders infer avg time:76.67 ms, vae encoder infer avg time:0.00 ms, vae decoder infer avg time:4470.33 ms
212214
```
215+
216+
### Run multiple generations with different prompt in parallel
217+
218+
It is highly recommended to use `ov::genai::num_images_per_prompt(X)` parameter to generate multiple images in parallel. However, when the generation options differ (prompt, height, width), it is recommended to clone the pipeline.
219+
It is possible to re-use models compiled into device for concurrent generation with different prompts in separate threads.
220+
221+
Here in this example we load and compile the entire pipeline once, and then use `clone()` to create separate generation requests to be reused in separate threads:
222+
223+
224+
```cpp
225+
std::vector<ov::genai::Text2ImagePipeline> pipelines;
226+
227+
// Prepare initial pipeline and compiled models into device
228+
pipelines.emplace_back(models_path, device);
229+
// Clone pipeline for concurrent usage
230+
for (size_t i = 1; i < 4; i++)
231+
pipelines.emplace_back(pipelines.begin()->clone());
232+
233+
std::vector<std::thread> threads;
234+
235+
for (size_t i = 0; i < 4; i++) {
236+
auto& pipe = pipelines.at(i);
237+
threads.emplace_back([&pipe, i] {
238+
std::string prompt = "A card with number " + std::to_string(i);
239+
240+
ov::Tensor image = pipe.generate(prompt,
241+
ov::AnyMap{
242+
ov::genai::width(512),
243+
ov::genai::height(512),
244+
ov::genai::num_inference_steps(25)});
245+
246+
// save image
247+
});
248+
}
249+
250+
for (auto& thread : threads) {
251+
thread.join();
252+
}
253+
```
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright (C) 2023-2025 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#include <vector>
5+
#include <string>
6+
7+
#include "openvino/genai/image_generation/image2image_pipeline.hpp"
8+
9+
#include "imwrite.hpp"
10+
#include "load_image.hpp"
11+
#include "progress_bar.hpp"
12+
13+
int32_t main(int32_t argc, char* argv[]) try {
14+
OPENVINO_ASSERT(argc >= 4, "Usage: ", argv[0], " <MODEL_DIR> '<PROMPT>' '<PROMPT>' ... <IMAGE>");
15+
16+
const std::string models_path = argv[1];
17+
const std::string device = "CPU"; // GPU can be used as well
18+
const std::string image_path = argv[argc - 1];
19+
ov::Tensor image = utils::load_image(image_path);
20+
21+
std::vector<std::thread> threads;
22+
std::vector<std::string> prompts;
23+
std::vector<ov::genai::Image2ImagePipeline> pipelines;
24+
25+
for (int32_t i = 2; i < argc - 1; ++i)
26+
prompts.push_back(argv[i]);
27+
28+
// Prepare initial pipeline and compiled models into device
29+
pipelines.emplace_back(models_path, device);
30+
31+
// Clone pipeline for concurrent usage
32+
for (size_t i = 1; i < prompts.size(); ++i)
33+
pipelines.emplace_back(pipelines.begin()->clone());
34+
35+
for (size_t i = 0; i < prompts.size(); ++i) {
36+
std::string prompt = prompts[i];
37+
auto& pipe = pipelines.at(i);
38+
39+
std::cout << "Starting to generate with prompt: '" << prompt << "'..." << std::endl;
40+
41+
threads.emplace_back([i, &pipe, prompt, image] () {
42+
43+
ov::Tensor generated_image = pipe.generate(prompt, image,
44+
// controls how initial image is noised after being converted to latent space. `1` means initial image is fully noised
45+
ov::genai::strength(0.8f),
46+
ov::genai::num_inference_steps(4));
47+
48+
// writes `num_images_per_prompt` images by pattern name
49+
imwrite("image_" + std::to_string(i) + "_%d.bmp", generated_image, true);
50+
});
51+
}
52+
53+
for (auto& thread : threads) {
54+
thread.join();
55+
}
56+
57+
return EXIT_SUCCESS;
58+
} catch (const std::exception& error) {
59+
try {
60+
std::cerr << error.what() << '\n';
61+
} catch (const std::ios_base::failure&) {}
62+
return EXIT_FAILURE;
63+
} catch (...) {
64+
try {
65+
std::cerr << "Non-exception object thrown\n";
66+
} catch (const std::ios_base::failure&) {}
67+
return EXIT_FAILURE;
68+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (C) 2023-2025 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
#include <iostream>
4+
5+
#include "openvino/genai/image_generation/text2image_pipeline.hpp"
6+
7+
8+
#include "imwrite.hpp"
9+
#include "progress_bar.hpp"
10+
11+
int32_t main(int32_t argc, char* argv[]) try {
12+
OPENVINO_ASSERT(argc >= 3, "Usage: ", argv[0], " <MODEL_DIR> '<PROMPT>' '<PROMPT>' ...");
13+
14+
const std::string models_path = argv[1];
15+
const std::string device = "CPU"; // GPU can be used as well
16+
17+
std::vector<std::thread> threads;
18+
std::vector<std::string> prompts;
19+
std::vector<ov::genai::Text2ImagePipeline> pipelines;
20+
21+
for (int i = 2; i < argc; ++i)
22+
prompts.push_back(argv[i]);
23+
24+
// Prepare initial pipeline and compiled models into device
25+
pipelines.emplace_back(models_path, device);
26+
27+
// Clone pipeline for concurrent usage
28+
for (size_t i = 1; i < prompts.size(); ++i)
29+
pipelines.emplace_back(pipelines.begin()->clone());
30+
31+
for (size_t i = 0; i < prompts.size(); ++i) {
32+
std::string prompt = prompts[i];
33+
auto& pipe = pipelines.at(i);
34+
35+
std::cout << "Starting to generate with prompt: '" << prompt << "'..." << std::endl;
36+
37+
threads.emplace_back([i, &pipe, prompt] () {
38+
39+
ov::Tensor image = pipe.generate(prompt,
40+
ov::AnyMap{
41+
ov::genai::width(512),
42+
ov::genai::height(512),
43+
ov::genai::num_inference_steps(2),
44+
ov::genai::num_images_per_prompt(1)});
45+
46+
imwrite("image_" + std::to_string(i) + "_%d.bmp", image, true);
47+
});
48+
}
49+
50+
for (auto& thread : threads) {
51+
thread.join();
52+
}
53+
54+
return EXIT_SUCCESS;
55+
} catch (const std::exception& error) {
56+
try {
57+
std::cerr << error.what() << '\n';
58+
} catch (const std::ios_base::failure&) {}
59+
return EXIT_FAILURE;
60+
} catch (...) {
61+
try {
62+
std::cerr << "Non-exception object thrown\n";
63+
} catch (const std::ios_base::failure&) {}
64+
return EXIT_FAILURE;
65+
}

src/cpp/include/openvino/genai/image_generation/autoencoder_kl.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ class OPENVINO_GENAI_EXPORTS AutoencoderKL {
116116

117117
AutoencoderKL(const AutoencoderKL&);
118118

119+
AutoencoderKL clone();
120+
119121
AutoencoderKL& reshape(int batch_size, int height, int width);
120122

121123
AutoencoderKL& compile(const std::string& device, const ov::AnyMap& properties = {});

src/cpp/include/openvino/genai/image_generation/clip_text_model.hpp

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#pragma once
55

66
#include <filesystem>
7+
#include <memory>
78
#include <string>
89

910
#include "openvino/genai/visibility.hpp"
@@ -69,6 +70,8 @@ class OPENVINO_GENAI_EXPORTS CLIPTextModel {
6970

7071
CLIPTextModel(const CLIPTextModel&);
7172

73+
std::shared_ptr<CLIPTextModel> clone();
74+
7275
const Config& get_config() const;
7376

7477
CLIPTextModel& reshape(int batch_size);
@@ -91,12 +94,12 @@ class OPENVINO_GENAI_EXPORTS CLIPTextModel {
9194
private:
9295
Config m_config;
9396
AdapterController m_adapter_controller;
94-
ov::InferRequest m_request;
95-
std::shared_ptr<ov::Model> m_model;
96-
9797
Tokenizer m_clip_tokenizer;
98-
9998
bool m_slice_batch1_output = false;
99+
100+
protected:
101+
ov::InferRequest m_request;
102+
std::shared_ptr<ov::Model> m_model;
100103
};
101104

102105
} // namespace genai

src/cpp/include/openvino/genai/image_generation/clip_text_model_with_projection.hpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,21 @@ namespace genai {
1111
class CLIPTextModelWithProjection : public CLIPTextModel {
1212
public:
1313
using CLIPTextModel::CLIPTextModel;
14+
15+
std::shared_ptr<CLIPTextModel> clone() {
16+
OPENVINO_ASSERT((m_model != nullptr) ^ static_cast<bool>(m_request), "CLIPTextModelWithProjection must have exactly one of m_model or m_request initialized");
17+
18+
std::shared_ptr<CLIPTextModelWithProjection> cloned = std::make_shared<CLIPTextModelWithProjection>(*this);
19+
20+
if (m_model) {
21+
cloned->m_model = m_model->clone();
22+
} else {
23+
cloned->m_request = m_request.get_compiled_model().create_infer_request();
24+
}
25+
26+
return cloned;
27+
}
28+
1429
};
1530

1631
} // namespace genai

src/cpp/include/openvino/genai/image_generation/flux_transformer_2d_model.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ class OPENVINO_GENAI_EXPORTS FluxTransformer2DModel {
6262

6363
FluxTransformer2DModel(const FluxTransformer2DModel&);
6464

65+
FluxTransformer2DModel clone();
66+
6567
const Config& get_config() const;
6668

6769
FluxTransformer2DModel& reshape(int batch_size, int height, int width, int tokenizer_model_max_length);

src/cpp/include/openvino/genai/image_generation/image2image_pipeline.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ class OPENVINO_GENAI_EXPORTS Image2ImagePipeline {
7373
const CLIPTextModelWithProjection& clip_text_model_2,
7474
const SD3Transformer2DModel& transformer,
7575
const AutoencoderKL& vae);
76+
77+
/**
78+
* Method to clone the pipeline to be used in parallel by another thread.
79+
* Reuses underlying models and recreates scheduler and generation config.
80+
* @returns A new pipeline for concurrent usage
81+
*/
82+
Image2ImagePipeline clone();
7683

7784
ImageGenerationConfig get_generation_config() const;
7885
void set_generation_config(const ImageGenerationConfig& generation_config);

src/cpp/include/openvino/genai/image_generation/sd3_transformer_2d_model.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ class OPENVINO_GENAI_EXPORTS SD3Transformer2DModel {
6363

6464
SD3Transformer2DModel(const SD3Transformer2DModel&);
6565

66+
SD3Transformer2DModel clone();
67+
6668
const Config& get_config() const;
6769

6870
SD3Transformer2DModel& reshape(int batch_size, int height, int width, int tokenizer_model_max_length);

0 commit comments

Comments
 (0)