Skip to content

Commit a3a2b8b

Browse files
[JS OV] Add Core.importModel() (#25258)
### Details: - Add natively asynchronous Core.importModel() - Add definition to addon.ts ### Tickets: - *136462* --------- Co-authored-by: Vishniakov Nikolai <nikolai.vishniakov@intel.com>
1 parent 64f22d9 commit a3a2b8b

File tree

6 files changed

+152
-8
lines changed

6 files changed

+152
-8
lines changed

src/bindings/js/node/include/core_wrap.hpp

+18
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ class CoreWrap : public Napi::ObjectWrap<CoreWrap> {
9191
/** @brief Imports a compiled model from the previously exported one. */
9292
Napi::Value import_model(const Napi::CallbackInfo& info);
9393

94+
/** @brief Implements Core.importModel() defined in ../lib/addon.ts. */
95+
Napi::Value import_model_async(const Napi::CallbackInfo& info);
96+
9497
/** @brief Returns devices available for inference. */
9598
Napi::Value get_available_devices(const Napi::CallbackInfo& info);
9699

@@ -99,6 +102,7 @@ class CoreWrap : public Napi::ObjectWrap<CoreWrap> {
99102

100103
private:
101104
ov::Core _core;
105+
std::mutex _mutex;
102106
};
103107

104108
struct TsfnContextModel {
@@ -127,6 +131,20 @@ struct TsfnContextPath {
127131
std::map<std::string, ov::Any> _config = {};
128132
};
129133

134+
struct ImportModelContext {
135+
ImportModelContext(Napi::Env env, ov::Core& core) : deferred(Napi::Promise::Deferred::New(env)), _core{core} {};
136+
std::thread nativeThread;
137+
138+
Napi::Promise::Deferred deferred;
139+
Napi::ThreadSafeFunction tsfn;
140+
141+
std::stringstream _stream;
142+
std::string _device;
143+
std::map<std::string, ov::Any> _config = {};
144+
ov::Core& _core;
145+
ov::CompiledModel _compiled_model;
146+
};
147+
130148
void FinalizerCallbackModel(Napi::Env env, void* finalizeData, TsfnContextModel* context);
131149
void FinalizerCallbackPath(Napi::Env env, void* finalizeData, TsfnContextPath* context);
132150
void compileModelThreadModel(TsfnContextModel* context);

src/bindings/js/node/include/helper.hpp

+2
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ Napi::Array cpp_to_js<ov::Dimension, Napi::Array>(const Napi::CallbackInfo& info
108108
template <>
109109
Napi::Boolean cpp_to_js<bool, Napi::Boolean>(const Napi::CallbackInfo& info, const bool value);
110110

111+
Napi::Object cpp_to_js(const Napi::Env& env, const ov::CompiledModel& compiled_model);
112+
111113
/** @brief Takes Napi::Value and parse Napi::Array or Napi::Object to ov::TensorVector. */
112114
ov::TensorVector parse_input_data(const Napi::Value& input);
113115

src/bindings/js/node/lib/addon.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ interface Core {
123123
},
124124
};
125125
/**
126-
* It imports a previously exported compiled model.
126+
* Asynchronously imports a previously exported compiled model.
127127
* @param modelStream The input stream that contains a model,
128128
* previously exported with the {@link CompiledModel.exportModelSync} method.
129129
* @param device The name of a device, for which you import a compiled model.
@@ -132,6 +132,15 @@ interface Core {
132132
* @param config An object with the key-value pairs
133133
* (property name, property value): relevant only for this load operation.
134134
*/
135+
importModel(
136+
modelStream: Buffer,
137+
device: string,
138+
config?: { [key: string]: string | number | boolean }
139+
): Promise<CompiledModel>;
140+
/**
141+
* A synchronous version of {@link Core.importModel}.
142+
* It imports a previously exported compiled model.
143+
*/
135144
importModelSync(
136145
modelStream: Buffer,
137146
device: string,

src/bindings/js/node/src/core_wrap.cpp

+61
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Napi::Function CoreWrap::get_class(Napi::Env env) {
5151
InstanceMethod("compileModelSync", &CoreWrap::compile_model_sync_dispatch),
5252
InstanceMethod("compileModel", &CoreWrap::compile_model_async),
5353
InstanceMethod("getAvailableDevices", &CoreWrap::get_available_devices),
54+
InstanceMethod("importModel", &CoreWrap::import_model_async),
5455
InstanceMethod("importModelSync", &CoreWrap::import_model),
5556
InstanceMethod("getAvailableDevices", &CoreWrap::get_available_devices),
5657
InstanceMethod("getVersions", &CoreWrap::get_versions),
@@ -350,6 +351,66 @@ Napi::Value CoreWrap::import_model(const Napi::CallbackInfo& info) {
350351
}
351352
}
352353

354+
void ImportModelFinalizer(Napi::Env env, void* finalizeData, ImportModelContext* context) {
355+
context->nativeThread.join();
356+
delete context;
357+
};
358+
359+
void importModelThread(ImportModelContext* context, std::mutex& mutex) {
360+
// Imports model without blocking the main thread.
361+
{
362+
const std::lock_guard<std::mutex> lock(mutex);
363+
context->_compiled_model = context->_core.import_model(context->_stream, context->_device, context->_config);
364+
}
365+
366+
// Callback to return to JS the results of core.import_model()
367+
auto callback = [](Napi::Env env, Napi::Function, ImportModelContext* context) {
368+
context->deferred.Resolve(cpp_to_js(env, context->_compiled_model));
369+
};
370+
371+
// Addon's main thread will safely invoke the JS callback function on the behalf of the additional thread.
372+
context->tsfn.BlockingCall(context, callback);
373+
context->tsfn.Release();
374+
}
375+
376+
Napi::Value CoreWrap::import_model_async(const Napi::CallbackInfo& info) {
377+
const auto& env = info.Env();
378+
std::vector<std::string> allowed_signatures;
379+
380+
try {
381+
if (ov::js::validate<Napi::Buffer<uint8_t>, Napi::String>(info, allowed_signatures) ||
382+
ov::js::validate<Napi::Buffer<uint8_t>, Napi::String, Napi::Object>(info, allowed_signatures)) {
383+
// Prepare validated data that will be transferred to the new thread.
384+
auto context_data = new ImportModelContext(env, _core);
385+
386+
const auto& model_data = info[0].As<Napi::Buffer<uint8_t>>();
387+
const auto model_stream = std::string(reinterpret_cast<char*>(model_data.Data()), model_data.Length());
388+
context_data->_stream << model_stream;
389+
context_data->_device = info[1].ToString();
390+
context_data->_config = info.Length() == 3 ? to_anyMap(env, info[2]) : ov::AnyMap();
391+
392+
context_data->tsfn = Napi::ThreadSafeFunction::New(env,
393+
Napi::Function(),
394+
"TSFN",
395+
0,
396+
1,
397+
context_data,
398+
ImportModelFinalizer,
399+
(void*)nullptr);
400+
401+
context_data->nativeThread = std::thread(importModelThread, context_data, std::ref(_mutex));
402+
// Returns a Promise to JS. Method import_model() is performed on additional thread.
403+
return context_data->deferred.Promise();
404+
} else {
405+
OPENVINO_THROW("'importModel'", ov::js::get_parameters_error_msg(info, allowed_signatures));
406+
}
407+
408+
} catch (std::exception& e) {
409+
reportError(info.Env(), e.what());
410+
return info.Env().Undefined();
411+
}
412+
}
413+
353414
Napi::Value CoreWrap::set_property(const Napi::CallbackInfo& info) {
354415
try {
355416
auto args = try_get_set_property_parameters(info);

src/bindings/js/node/src/helper.cpp

+12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include "node/include/helper.hpp"
55

6+
#include "node/include/compiled_model.hpp"
67
#include "node/include/tensor.hpp"
78
#include "node/include/type_validation.hpp"
89

@@ -256,6 +257,17 @@ Napi::Boolean cpp_to_js<bool, Napi::Boolean>(const Napi::CallbackInfo& info, con
256257
return Napi::Boolean::New(info.Env(), value);
257258
}
258259

260+
Napi::Object cpp_to_js(const Napi::Env& env, const ov::CompiledModel& compiled_model) {
261+
const auto& prototype = env.GetInstanceData<AddonData>()->compiled_model;
262+
if (!prototype) {
263+
OPENVINO_THROW("Invalid pointer to CompiledModel prototype.");
264+
}
265+
auto obj = prototype.New({});
266+
const auto cm = Napi::ObjectWrap<CompiledModelWrap>::Unwrap(obj);
267+
cm->set_compiled_model(compiled_model);
268+
return obj;
269+
}
270+
259271
ov::TensorVector parse_input_data(const Napi::Value& input) {
260272
ov::TensorVector parsed_input;
261273
if (input.IsArray()) {

src/bindings/js/node/tests/basic.test.js

+49-7
Original file line numberDiff line numberDiff line change
@@ -236,55 +236,97 @@ describe('Test exportModel()/importModel()', () => {
236236
const inferRequest = compiledModel.createInferRequest();
237237
const res1 = inferRequest.infer([tensor]);
238238

239-
it('Test importModel(stream, device)', () => {
239+
it('Test importModelSync(stream, device)', () => {
240240
const newCompiled = core.importModelSync(userStream, 'CPU');
241241
const newInferRequest = newCompiled.createInferRequest();
242242
const res2 = newInferRequest.infer([tensor]);
243243

244244
assert.deepStrictEqual(res1['fc_out'].data[0], res2['fc_out'].data[0]);
245245
});
246246

247-
it('Test importModel(stream, device, config)', () => {
247+
it('Test importModelSync(stream, device, config)', () => {
248248
const newCompiled = core.importModelSync(userStream, 'CPU', { 'NUM_STREAMS': 1 });
249249
const newInferRequest = newCompiled.createInferRequest();
250250
const res2 = newInferRequest.infer([tensor]);
251251

252252
assert.deepStrictEqual(res1['fc_out'].data[0], res2['fc_out'].data[0]);
253253
});
254254

255-
it('Test importModel(stream, device) throws', () => {
255+
it('Test importModelSync(stream, device) throws', () => {
256256
assert.throws(
257257
() => core.importModelSync(epsilon, 'CPU'),
258258
/The first argument must be of type Buffer./
259259
);
260260
});
261261

262-
it('Test importModel(stream, device) throws', () => {
262+
it('Test importModelSync(stream, device) throws', () => {
263263
assert.throws(
264264
() => core.importModelSync(userStream, tensor),
265265
/The second argument must be of type String./
266266
);
267267
});
268-
it('Test importModel(stream, device, config: tensor) throws', () => {
268+
it('Test importModelSync(stream, device, config: tensor) throws', () => {
269269
assert.throws(
270270
() => core.importModelSync(userStream, 'CPU', tensor),
271271
/NotFound: Unsupported property 0 by CPU plugin./
272272
);
273273
});
274274

275-
it('Test importModel(stream, device, config: string) throws', () => {
275+
it('Test importModelSync(stream, device, config: string) throws', () => {
276276
const testString = 'test';
277277
assert.throws(
278278
() => core.importModelSync(userStream, 'CPU', testString),
279279
/Passed Napi::Value must be an object./
280280
);
281281
});
282282

283-
it('Test importModel(stream, device, config: unsupported property) throws', () => {
283+
it('Test importModelSync(stream, device, config: unsupported property) \
284+
throws', () => {
284285
const tmpDir = '/tmp';
285286
assert.throws(
286287
() => core.importModelSync(userStream, 'CPU', { 'CACHE_DIR': tmpDir }),
287288
/Unsupported property CACHE_DIR by CPU plugin./
288289
);
289290
});
291+
292+
it('Test importModel(stream, device)', () => {
293+
core.importModel(userStream, 'CPU').then(newCompiled => {
294+
const newInferRequest = newCompiled.createInferRequest();
295+
const res2 = newInferRequest.infer([tensor]);
296+
assert.deepStrictEqual(res1['fc_out'].data[0], res2['fc_out'].data[0]);
297+
});
298+
});
299+
300+
it('Test importModel(stream, device, config)', () => {
301+
core.importModel(userStream, 'CPU', { 'NUM_STREAMS': 1 }).then(
302+
newCompiled => {
303+
const newInferRequest = newCompiled.createInferRequest();
304+
const res2 = newInferRequest.infer([tensor]);
305+
306+
assert.deepStrictEqual(res1['fc_out'].data[0], res2['fc_out'].data[0]);
307+
});
308+
});
309+
310+
it('Test importModel(stream, device) throws', () => {
311+
assert.throws(
312+
() => core.importModel(epsilon, 'CPU').then(),
313+
/'importModel' method called with incorrect parameters./
314+
);
315+
});
316+
317+
it('Test importModel(stream, device) throws', () => {
318+
assert.throws(
319+
() => core.importModel(userStream, tensor).then(),
320+
/'importModel' method called with incorrect parameters./
321+
);
322+
});
323+
324+
it('Test importModel(stream, device, config: string) throws', () => {
325+
const testString = 'test';
326+
assert.throws(
327+
() => core.importModel(userStream, 'CPU', testString).then(),
328+
/'importModel' method called with incorrect parameters./
329+
);
330+
});
331+
290332
});

0 commit comments

Comments
 (0)