Skip to content
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

Open
wants to merge 16 commits into
base: master
Choose a base branch
from

Conversation

hub-bla
Copy link
Contributor

@hub-bla hub-bla commented Oct 20, 2024

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:

@hub-bla hub-bla requested a review from a team as a code owner October 20, 2024 09:42
@hub-bla hub-bla marked this pull request as draft October 20, 2024 09:42
@github-actions github-actions bot added the category: JS API OpenVino JS API Bindings label Oct 20, 2024
@sys-openvino-ci sys-openvino-ci added the ExternalPR External contributor label Oct 20, 2024
Copy link
Contributor

@almilosz almilosz left a 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;
Copy link
Contributor

Choose a reason for hiding this comment

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

Those FunctionReferences 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

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

@@ -675,6 +675,16 @@ export interface NodeAddon {
resizeAlgorithm: typeof resizeAlgorithm;
PrePostProcessor: PrePostProcessorConstructor;
};

/**
* It saves model in a specified path.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* 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.

Comment on lines 681 to 684
* @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`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* @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`.

* @param [compressToFp16] [OPTIONAL] Whether to compress the model
* to float16. Default is set to `true`.
*/
saveModel(model:Model, path:string, compressToFp16?:boolean) : void;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
saveModel(model:Model, path:string, compressToFp16?:boolean) : void;
saveModel(model: Model, path: string, compressToFp16?: boolean): void;

eslint does not fix everything unfortunately :/

@almilosz
Copy link
Contributor

For now you don't have to include Model.getOps() method in tests. Please focus on other characteristics e.g. name, number of outputs etc.

const epsilon = 0.5;
const outDir = 'tests/unit/out';
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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`;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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));
Copy link
Contributor

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)',)

Suggested change
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));
Copy link
Contributor

@vishniakov-nikolai vishniakov-nikolai Oct 21, 2024

Choose a reason for hiding this comment

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

Because false by default

Suggested change
assert.doesNotThrow(() => ov.saveModel(modelLike[0], xmlPath, false));
assert.doesNotThrow(() => ov.saveModel(modelLike[0], xmlPath));

Comment on lines 34 to 53
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);
}
Copy link
Contributor

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

Suggested change
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('../..');
Copy link
Contributor

Choose a reason for hiding this comment

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

Why?

Copy link
Contributor Author

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));
});
});
Copy link
Contributor

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

Copy link
Contributor Author

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.

@@ -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);
Copy link
Contributor

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:

Suggested change
init_function(env, exports, "saveModel", save_model, addon_data->save_model);
init_function(env, exports, "saveModelSync", save_model, addon_data->save_model);

Copy link
Contributor Author

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?

Copy link
Contributor

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

@@ -27,6 +30,30 @@ void init_class(Napi::Env env,
exports.Set(class_name, prototype);
}

Napi::Value save_model(const Napi::CallbackInfo& info) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Napi::Value save_model(const Napi::CallbackInfo& info) {
Napi::Value save_model_sync(const Napi::CallbackInfo& info) {

@vishniakov-nikolai vishniakov-nikolai added this to the 2024.5 milestone Oct 21, 2024
@hub-bla hub-bla marked this pull request as ready for review October 26, 2024 12:30
@hub-bla
Copy link
Contributor Author

hub-bla commented Oct 26, 2024

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!

Copy link
Contributor

@vishniakov-nikolai vishniakov-nikolai left a 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

src/bindings/js/node/lib/addon.ts Outdated Show resolved Hide resolved
const epsilon = 0.5;
const outDir = 'tests/unit/out/';
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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()

Copy link
Contributor

@almilosz almilosz Oct 29, 2024

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

const epsilon = 0.5;
const outDir = 'tests/unit/out/';
Copy link
Contributor

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()

@vishniakov-nikolai
Copy link
Contributor

@hub-bla to archive what Alicja suggested you may:

  • generate random folder name using fs.mkdtemp
  • put this folder by system tmp path (get by os.tmpdir())
  • use it to store saved model files
  • add after hook (const { after } = require('node:test');) into unit test to clean up temporary folder after tests execution despite test result

@hub-bla hub-bla requested a review from almilosz October 30, 2024 19:57
@hub-bla
Copy link
Contributor Author

hub-bla commented Oct 30, 2024

@almilosz @vishniakov-nikolai Thank you for clarification! I adjusted it based on the guidelines.

Copy link
Contributor

@almilosz almilosz left a comment

Choose a reason for hiding this comment

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

Thanks for contribution!

@vishniakov-nikolai
Copy link
Contributor

build_jenkins

@mlukasze mlukasze modified the milestones: 2024.5, 2025.0 Nov 6, 2024
@mlukasze
Copy link
Contributor

mlukasze commented Nov 6, 2024

build_jenkins

@vishniakov-nikolai
Copy link
Contributor

Hi @hub-bla!
Something went wrong with tests: https://github.com/openvinotoolkit/openvino/actions/runs/11722356830/job/32653961578?pr=27148#step:6:33
Please, take a look.

Attached screenshot if you cannot see GHA pipeline logs:
{1DF0586C-7873-45CB-9469-54276F7E9C71}

auto-merge was automatically disabled November 9, 2024 14:13

Head branch was pushed to by a user without write access

@hub-bla
Copy link
Contributor Author

hub-bla commented Nov 9, 2024

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 function with mkdtemp, and if that doesn’t resolve the issue, I'll set it up on Windows to investigate further.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
category: JS API OpenVino JS API Bindings ExternalPR External contributor
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants