-
Notifications
You must be signed in to change notification settings - Fork 2.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[OV JS]: Expose ov.saveModel() functionality #27148
[OV JS]: Expose ov.saveModel() functionality #27148
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for taking up this task! It's not trivial. I have a few comments :)
@@ -18,6 +18,7 @@ struct AddonData { | |||
Napi::FunctionReference partial_shape; | |||
Napi::FunctionReference ppp; | |||
Napi::FunctionReference tensor; | |||
Napi::FunctionReference save_model; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those FunctionReference
s keep the class prototypes functions and later are used for type validation. I don't see the usecase for save_model
and propose to remove it from here and init_function
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the explanation. Makes sense now!
Quick question regarding init_function
. After removing lines related to FunctionReference
it will contain only this line
Napi::Function::New(env, func, func_name);
, so I propose removing init_function
as well and replace it with the line above inside init_module
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I forgot that exports.Set
is also necessary, so I'll keep init_function
.
src/bindings/js/node/lib/addon.ts
Outdated
@@ -675,6 +675,16 @@ export interface NodeAddon { | |||
resizeAlgorithm: typeof resizeAlgorithm; | |||
PrePostProcessor: PrePostProcessorConstructor; | |||
}; | |||
|
|||
/** | |||
* It saves model in a specified path. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* It saves model in a specified path. | |
* It saves a model into IR files (xml and bin). Floating point weights are compressed to FP16 by default. This method saves a model to IR applying all necessary transformations that usually applied in model conversion flow provided by mo tool. Paricularly, floatting point weights are compressed to FP16, debug information in model nodes are cleaned up, etc. |
src/bindings/js/node/lib/addon.ts
Outdated
* @param [model] Model that will be saved. | ||
* @param [path] Path for saving the model. | ||
* @param [compressToFp16] [OPTIONAL] Whether to compress the model | ||
* to float16. Default is set to `true`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* @param [model] Model that will be saved. | |
* @param [path] Path for saving the model. | |
* @param [compressToFp16] [OPTIONAL] Whether to compress the model | |
* to float16. Default is set to `true`. | |
* @param model The model which will be converted to IR representation and saved. | |
* @param path The path for saving the model. | |
* @param compressToFp16 Whether to compress floating point weights to FP16. Default is set to `true`. |
src/bindings/js/node/lib/addon.ts
Outdated
* @param [compressToFp16] [OPTIONAL] Whether to compress the model | ||
* to float16. Default is set to `true`. | ||
*/ | ||
saveModel(model:Model, path:string, compressToFp16?:boolean) : void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
saveModel(model:Model, path:string, compressToFp16?:boolean) : void; | |
saveModel(model: Model, path: string, compressToFp16?: boolean): void; |
eslint does not fix everything unfortunately :/
For now you don't have to include |
const epsilon = 0.5; | ||
const outDir = 'tests/unit/out'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const outDir = 'tests/unit/out'; | |
const outDir = 'tests/unit/out/'; |
@@ -33,6 +34,24 @@ describe('ov basic tests.', () => { | |||
assert.ok(devices.includes('CPU')); | |||
}); | |||
|
|||
describe('ov.saveModel()', () => { | |||
it('saveModel(model:Model, path:string, compressToFp16:bool=true)', () => { | |||
const xmlPath = `${outDir}/${modelLike[0].getName()}_fp16.xml`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const xmlPath = `${outDir}/${modelLike[0].getName()}_fp16.xml`; | |
const xmlPath = `${outDir}${modelLike[0].getName()}_fp16.xml`; |
describe('ov.saveModel()', () => { | ||
it('saveModel(model:Model, path:string, compressToFp16:bool=true)', () => { | ||
const xmlPath = `${outDir}/${modelLike[0].getName()}_fp16.xml`; | ||
assert.doesNotThrow(() => ov.saveModel(modelLike[0], xmlPath)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean? (it('saveModel(model:Model, path:string, compressToFp16:bool=true)',
)
assert.doesNotThrow(() => ov.saveModel(modelLike[0], xmlPath)); | |
assert.doesNotThrow(() => ov.saveModel(modelLike[0], xmlPath, true)); |
|
||
it('saveModel(model:Model, path:string, compressToFp16:bool)', () => { | ||
const xmlPath = `${outDir}/${modelLike[0].getName()}_fp32.xml`; | ||
assert.doesNotThrow(() => ov.saveModel(modelLike[0], xmlPath, false)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because false by default
assert.doesNotThrow(() => ov.saveModel(modelLike[0], xmlPath, false)); | |
assert.doesNotThrow(() => ov.saveModel(modelLike[0], xmlPath)); |
let msg = ''; | ||
if (compareNames && model1.getFriendlyName() !== model2.getFriendlyName()) { | ||
msg += 'Friendly names of models are not equal '; | ||
msg += `model_one: ${model1.getFriendlyName()}, | ||
model_two: ${model2.getFriendlyName()}`; | ||
} | ||
|
||
if (model1.inputs.length !== model2.inputs.length) { | ||
msg += 'Number of models\' inputs are not equal '; | ||
msg += `model_one: ${model1.inputs.length}, `; | ||
msg += `model_two: ${model2.inputs.length}`; | ||
throw new Error(msg); | ||
} | ||
|
||
if (model1.outputs.length !== model2.outputs.length) { | ||
msg += 'Number of models\' outputs are not equal '; | ||
msg += `model_one: ${model1.outputs.length}, | ||
model_two: ${model2.outputs.length}`; | ||
throw new Error(msg); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Didn't catch where compareNames
using
let msg = ''; | |
if (compareNames && model1.getFriendlyName() !== model2.getFriendlyName()) { | |
msg += 'Friendly names of models are not equal '; | |
msg += `model_one: ${model1.getFriendlyName()}, | |
model_two: ${model2.getFriendlyName()}`; | |
} | |
if (model1.inputs.length !== model2.inputs.length) { | |
msg += 'Number of models\' inputs are not equal '; | |
msg += `model_one: ${model1.inputs.length}, `; | |
msg += `model_two: ${model2.inputs.length}`; | |
throw new Error(msg); | |
} | |
if (model1.outputs.length !== model2.outputs.length) { | |
msg += 'Number of models\' outputs are not equal '; | |
msg += `model_one: ${model1.outputs.length}, | |
model_two: ${model2.outputs.length}`; | |
throw new Error(msg); | |
} | |
const deviations = []; | |
if (model1.getFriendlyName() !== model2.getFriendlyName()) | |
deviations.push('Friendly names of models are not equal ' | |
+ `model_one: ${model1.getFriendlyName()}, ` | |
+ `model_two: ${model2.getFriendlyName()}`); | |
if (model1.inputs.length !== model2.inputs.length) | |
deviations.push('Number of models\' inputs are not equal ' | |
+ `model_one: ${model1.inputs.length}, ` | |
+ `model_two: ${model2.inputs.length}`); | |
if (model1.outputs.length !== model2.outputs.length) | |
deviations.push('Number of models\' outputs are not equal ' | |
+ `model_one: ${model1.outputs.length}, ` | |
+ `model_two: ${model2.outputs.length}`); | |
if (deviations.length) throw new Error(deviations.join('\n')); |
@@ -2,6 +2,7 @@ | |||
// Copyright (C) 2018-2024 Intel Corporation | |||
// SPDX-License-Identifier: Apache-2.0 | |||
|
|||
const { addon: ov } = require('../..'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought it would be handy in comparison function but it hasn't been necessary so far. I'll delete it.
const savedModel = core.readModelSync(xmlPath); | ||
assert.doesNotThrow(() => compareModels(modelLike[0], savedModel)); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Propose to add more test that check invalid parameters passing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Absolutely, it's just rough draft for now.
src/bindings/js/node/src/addon.cpp
Outdated
@@ -41,6 +68,8 @@ Napi::Object init_module(Napi::Env env, Napi::Object exports) { | |||
init_class(env, exports, "ConstOutput", &Output<const ov::Node>::get_class, addon_data->const_output); | |||
init_class(env, exports, "PartialShape", &PartialShapeWrap::get_class, addon_data->partial_shape); | |||
|
|||
init_function(env, exports, "saveModel", save_model, addon_data->save_model); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Function that blocks event loop must have postfix Sync
:
init_function(env, exports, "saveModel", save_model, addon_data->save_model); | |
init_function(env, exports, "saveModelSync", save_model, addon_data->save_model); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should I also make async
version?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is enough to have sync version as the solution of this issue
src/bindings/js/node/src/addon.cpp
Outdated
@@ -27,6 +30,30 @@ void init_class(Napi::Env env, | |||
exports.Set(class_name, prototype); | |||
} | |||
|
|||
Napi::Value save_model(const Napi::CallbackInfo& info) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Napi::Value save_model(const Napi::CallbackInfo& info) { | |
Napi::Value save_model_sync(const Napi::CallbackInfo& info) { |
I’ve made the proposed changes and added tests to cover cases with invalid arguments. Let me know if there’s anything else you’d like me to adjust! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
LGTM, couple comments
const epsilon = 0.5; | ||
const outDir = 'tests/unit/out/'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does after unit tests run git has clear state?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, I believe I should add tests/unit/out
to .gitignore
, similar to how it was done for tests/unit/test_models
path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use mkdtemp(). The prefix can be simple. You may also see this usage of os.tempdir()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding files to .gitignore
won't remove files in the CI's check. So you should make sure they are deleted
Co-authored-by: Vishniakov Nikolai <[email protected]>
const epsilon = 0.5; | ||
const outDir = 'tests/unit/out/'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use mkdtemp(). The prefix can be simple. You may also see this usage of os.tempdir()
@hub-bla to archive what Alicja suggested you may:
|
@almilosz @vishniakov-nikolai Thank you for clarification! I adjusted it based on the guidelines. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for contribution!
build_jenkins |
build_jenkins |
Hi @hub-bla! |
Head branch was pushed to by a user without write access
This reverts commit 03015d0.
Hi @vishniakov-nikolai! It seems that the issue is specific to Windows. I initially tested it only on Ubuntu, as that’s the system I’m using. I’ve added a separate |
before(() => { | ||
fs.mkdtemp(path.join(os.tmpdir(), 'ov_js_out_'), (err, directory) => { | ||
if (err) { | ||
throw err; | ||
} | ||
outDir = directory; | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@hub-bla try to union two types of before in one.
Like:
before(async () => {
outDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'ov_js_out_'));
await isModelAvailable(testModels.testModelFP32);
testXml = getModelPath().xml;
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should resolve issue that happens on windows
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
build_jenkins |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is still issue with model file removal
Co-authored-by: Vishniakov Nikolai <[email protected]>
Co-authored-by: Vishniakov Nikolai <[email protected]>
Co-authored-by: Vishniakov Nikolai <[email protected]>
Co-authored-by: Vishniakov Nikolai <[email protected]>
Co-authored-by: Vishniakov Nikolai <[email protected]>
build_jenkins |
### Details: - Created `init_function` to initialize functions directly in the addon. - Extended `type_validation` with `Napi::Boolean` - Exposed `ov.saveModel` - To make proper tests like in Python API I guess I have to wait for `Model.getOps()` to be exposed. ### Tickets: - Closes - openvinotoolkit#27092 --------- Co-authored-by: Vishniakov Nikolai <[email protected]> Co-authored-by: Michal Lukaszewski <[email protected]>
Details:
init_function
to initialize functions directly in the addon.type_validation
withNapi::Boolean
ov.saveModel
Model.getOps()
to be exposed.Tickets: