From f79109963afc1ccce394b13d56e85fa3debe36de Mon Sep 17 00:00:00 2001 From: Gorokhov Dmitriy Date: Wed, 21 Aug 2024 16:04:57 +0400 Subject: [PATCH 01/35] [CPU] Fix debug assertion in oneDNN Matmul Brgemm kernel (#26087) ### Details: - OneDNN Matmul Brgemm kernel contains wrong debug assertion which lead to functional failurs on systems with avx2_vnni support. ### Tickets: - [CVS-149880](https://jira.devtools.intel.com/browse/CVS-149880) --- src/plugins/intel_cpu/thirdparty/onednn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/intel_cpu/thirdparty/onednn b/src/plugins/intel_cpu/thirdparty/onednn index 6b99866a4531e3..6237cb7cc6df35 160000 --- a/src/plugins/intel_cpu/thirdparty/onednn +++ b/src/plugins/intel_cpu/thirdparty/onednn @@ -1 +1 @@ -Subproject commit 6b99866a4531e38a74d1de36d5b366c54c5e6cc3 +Subproject commit 6237cb7cc6df3591bf0113fdc546fab724d4d55b From a07e2bc449a9cb4f0dc2a1e1f6dca97f3acd49d3 Mon Sep 17 00:00:00 2001 From: Alicja Miloszewska Date: Wed, 21 Aug 2024 15:34:06 +0200 Subject: [PATCH 02/35] [OV JS] Remove extra assets from tests (#25712) ### Details: - Remove testing models from repository - Add `setup.js` script that will be called once to download testing models - Before running the suite check if model is downloaded, add top describe to *.test.js files ### Notes: Currently there is no global setup file for native [node test runner](https://github.com/nodejs/node/issues/49732) as in other test runner e.g. [jest](https://jestjs.io/docs/configuration#globalsetup-string) ### Tickets: - [CVS-146345](https://jira.devtools.intel.com/browse/CVS-146345) --------- Co-authored-by: Vishniakov Nikolai --- src/bindings/js/node/.gitignore | 1 + src/bindings/js/node/package.json | 3 +- .../js/node/scripts/download_runtime.js | 18 +- src/bindings/js/node/tests/unit/basic.test.js | 481 +++++++++--------- .../js/node/tests/unit/compiled_model.test.js | 46 +- src/bindings/js/node/tests/unit/core.test.js | 85 ++-- .../js/node/tests/unit/infer_request.test.js | 469 ++++++++--------- src/bindings/js/node/tests/unit/model.test.js | 211 ++++---- .../js/node/tests/unit/partial_shape.test.js | 20 +- .../tests/unit/pre_post_processor.test.js | 244 ++++----- .../js/node/tests/unit/read_model.test.js | 137 ++--- src/bindings/js/node/tests/unit/setup.js | 9 + .../js/node/tests/unit/tensor.test.js | 397 ++++++++------- .../unit/test_models/test_model_fp32.bin | Bin 337592 -> 0 bytes .../unit/test_models/test_model_fp32.xml | 467 ----------------- src/bindings/js/node/tests/unit/utils.js | 60 ++- 16 files changed, 1147 insertions(+), 1501 deletions(-) create mode 100644 src/bindings/js/node/tests/unit/setup.js delete mode 100644 src/bindings/js/node/tests/unit/test_models/test_model_fp32.bin delete mode 100644 src/bindings/js/node/tests/unit/test_models/test_model_fp32.xml diff --git a/src/bindings/js/node/.gitignore b/src/bindings/js/node/.gitignore index 6bbca841fe2db9..38782682575826 100644 --- a/src/bindings/js/node/.gitignore +++ b/src/bindings/js/node/.gitignore @@ -3,6 +3,7 @@ dist build types ov_runtime +tests/unit/test_models *.exp diff --git a/src/bindings/js/node/package.json b/src/bindings/js/node/package.json index 43f0a264571c9e..a3544a6136cccf 100644 --- a/src/bindings/js/node/package.json +++ b/src/bindings/js/node/package.json @@ -18,7 +18,8 @@ "build": "npm run tsc", "prepare": "npm run build", "lint": "eslint .", - "test": "node --test ./tests/unit/*.test.js", + "test_setup": "node ./tests/unit/setup.js", + "test": "npm run test_setup && node --test ./tests/unit/*.test.js", "test:e2e": "mocha ./tests/e2e/electron-app.test.js", "tsc": "tsc", "postinstall": "npm run install_runtime", diff --git a/src/bindings/js/node/scripts/download_runtime.js b/src/bindings/js/node/scripts/download_runtime.js index f3aa3dce849c4d..775b4aa6c754c9 100644 --- a/src/bindings/js/node/scripts/download_runtime.js +++ b/src/bindings/js/node/scripts/download_runtime.js @@ -61,7 +61,7 @@ class RuntimeExistsError extends Error { async function downloadRuntime(destinationPath, config = { force: false, ignoreIfExists: true, proxy: null }) { const { version } = packageJson; const osInfo = await getOsInfo(); - const isRuntimeDirectoryExists = await checkIfDirectoryExists(destinationPath); + const isRuntimeDirectoryExists = await checkIfPathExists(destinationPath); if (isRuntimeDirectoryExists && !config.force) { if (config.ignoreIfExists) { @@ -87,7 +87,7 @@ async function downloadRuntime(destinationPath, config = { force: false, ignoreI await fs.mkdir(tempDirectoryPath); console.log('Downloading OpenVINO runtime archive...'); - await downloadFile(runtimeArchiveUrl, filename, tempDirectoryPath, config.proxy); + await downloadFile(runtimeArchiveUrl, tempDirectoryPath, filename, config.proxy); console.log('OpenVINO runtime archive downloaded.'); await removeDirectory(destinationPath); @@ -139,16 +139,16 @@ async function getOsInfo() { } /** - * Check if directory exists. + * Check if path exists. * * @async - * @function checkIfDirectoryExists - * @param {string} directoryPath - The directory path. + * @function checkIfPathExists + * @param {string} path - The path to directory or file. * @returns {Promise} */ -async function checkIfDirectoryExists(directoryPath) { +async function checkIfPathExists(path) { try { - await fs.access(directoryPath); + await fs.access(path); return true; } catch (error) { if (error.code === codeENOENT) { @@ -210,7 +210,7 @@ async function removeDirectory(path) { * @param {string} [proxy=null] - (Optional) The proxy URL. * @returns {Promise} */ -function downloadFile(url, filename, destination, proxy = null) { +function downloadFile(url, destination, filename, proxy = null) { const timeout = 5000; const fullPath = path.resolve(destination, filename); const file = createWriteStream(fullPath); @@ -281,4 +281,4 @@ function unarchive(tarFilePath, dest) { }); } -module.exports = { downloadRuntime }; +module.exports = { downloadRuntime, downloadFile, checkIfPathExists }; diff --git a/src/bindings/js/node/tests/unit/basic.test.js b/src/bindings/js/node/tests/unit/basic.test.js index e7447b3c863655..4b4913fb52b7d2 100644 --- a/src/bindings/js/node/tests/unit/basic.test.js +++ b/src/bindings/js/node/tests/unit/basic.test.js @@ -4,78 +4,83 @@ const { addon: ov } = require('../..'); const assert = require('assert'); -const { describe, it } = require('node:test'); -const { getModelPath } = require('./utils.js'); +const { describe, it, before } = require('node:test'); +const { testModels, getModelPath, isModelAvailable } = require('./utils.js'); -const testXml = getModelPath().xml; -const core = new ov.Core(); -const model = core.readModelSync(testXml); -const compiledModel = core.compileModelSync(model, 'CPU'); -const modelLike = [[model], - [compiledModel]]; +describe('ov basic tests.', () => { -it('Core.getAvailableDevices()', () => { - const devices = core.getAvailableDevices(); - - assert.ok(devices.includes('CPU')); -}); - -describe('Core.getVersions()', () => { - - it('getVersions(validDeviceName: string)', () => { - const deviceVersion = core.getVersions('CPU'); - assert.strictEqual(typeof deviceVersion, 'object'); - assert.strictEqual(typeof deviceVersion.CPU, 'object'); - assert.strictEqual(typeof deviceVersion.CPU.buildNumber, 'string'); - assert.strictEqual(typeof deviceVersion.CPU.description, 'string'); + before(async () => { + await isModelAvailable(testModels.testModelFP32); }); - it('getVersions() throws if no arguments are passed into the function', () => { - assert.throws( - () => core.getVersions(), - { message: 'getVersions() method expects 1 argument of string type.' } - ); - }); + const testXml = getModelPath().xml; + const core = new ov.Core(); + const model = core.readModelSync(testXml); + const compiledModel = core.compileModelSync(model, 'CPU'); + const modelLike = [[model], + [compiledModel]]; - it('getVersions() throws if non string coercable arguments are passed into the function', () => { - assert.throws( - () => core.getVersions({ deviceName: 'CPU' }), - { message: 'The argument in getVersions() method must be a string or convertible to a string.' } - ); - }); + it('Core.getAvailableDevices()', () => { + const devices = core.getAvailableDevices(); -}); + assert.ok(devices.includes('CPU')); + }); + describe('Core.getVersions()', () => { -it('CompiledModel type', () => { - assert.ok(compiledModel instanceof ov.CompiledModel); -}); + it('getVersions(validDeviceName: string)', () => { + const deviceVersion = core.getVersions('CPU'); + assert.strictEqual(typeof deviceVersion, 'object'); + assert.strictEqual(typeof deviceVersion.CPU, 'object'); + assert.strictEqual(typeof deviceVersion.CPU.buildNumber, 'string'); + assert.strictEqual(typeof deviceVersion.CPU.description, 'string'); + }); -it('compileModel.createInferRequest()', () => { - const ir = compiledModel.createInferRequest(); - assert.ok(ir instanceof ov.InferRequest); -}); + it('getVersions() throws if no arguments are passed into the function', () => { + assert.throws( + () => core.getVersions(), + { message: 'getVersions() method expects 1 argument of string type.' } + ); + }); -describe('Core.compileModelSync()', () => { - const tput = { 'PERFORMANCE_HINT': 'THROUGHPUT' }; + it('getVersions() throws if non string coercable arguments are passed into the function', () => { + assert.throws( + () => core.getVersions({ deviceName: 'CPU' }), + { message: 'The argument in getVersions() method must be a string or convertible to a string.' } + ); + }); - it('compileModelSync(model:Model, deviceName: string, config: {}) ', () => { - const cm = core.compileModelSync(model, 'CPU', tput); - assert.deepStrictEqual(cm.output(0).shape, [1, 10]); }); - it('compileModelSync(model:model_path, deviceName: string, config: {}) ', () => { - const cm = core.compileModelSync(testXml, 'CPU', tput); - assert.equal(cm.inputs.length, 1); + it('CompiledModel type', () => { + assert.ok(compiledModel instanceof ov.CompiledModel); }); - it('compileModelSync(model:model_path, deviceName: string) ', () => { - const cm = core.compileModelSync(testXml, 'CPU'); - assert.deepStrictEqual(cm.output(0).shape, [1, 10]); + it('compileModel.createInferRequest()', () => { + const ir = compiledModel.createInferRequest(); + assert.ok(ir instanceof ov.InferRequest); }); - it('compileModelSync(model, device, config) throws when config is a string', () => { - const expectedMsg = ("'compileModelSync' method called with incorrect parameters.\n" + + describe('Core.compileModelSync()', () => { + const tput = { 'PERFORMANCE_HINT': 'THROUGHPUT' }; + + it('compileModelSync(model:Model, deviceName: string, config: {}) ', () => { + const cm = core.compileModelSync(model, 'CPU', tput); + assert.deepStrictEqual(cm.output(0).shape, [1, 10]); + }); + + it('compileModelSync(model:model_path, deviceName: string, config: {}) ', () => { + const cm = core.compileModelSync(testXml, 'CPU', tput); + assert.equal(cm.inputs.length, 1); + }); + + it('compileModelSync(model:model_path, deviceName: string) ', () => { + const cm = core.compileModelSync(testXml, 'CPU'); + assert.deepStrictEqual(cm.output(0).shape, [1, 10]); + }); + + it('compileModelSync(model, device, config) throws when config is a string', () => { + const expectedMsg = ('\'compileModelSync\' method called with incorrect parameters.\n' + 'Provided signature: (object, string, string) \n' + 'Allowed signatures:\n' + '- (string, string)\n' + @@ -83,21 +88,21 @@ describe('Core.compileModelSync()', () => { '- (string, string, object)\n' + '- (Model, string, object)\n').replace(/[()]/g, '\\$&'); - assert.throws( - () => core.compileModelSync(model, 'CPU', 'string'), - new RegExp(expectedMsg), - ); - }); + assert.throws( + () => core.compileModelSync(model, 'CPU', 'string'), + new RegExp(expectedMsg), + ); + }); - it('compileModelSync(model, device, config) throws when config value is not a string', () => { - assert.throws( - () => core.compileModelSync(model, 'CPU', { 'PERFORMANCE_HINT': tput }), - /Cannot convert to ov::Any/ - ); - }); + it('compileModelSync(model, device, config) throws when config value is not a string', () => { + assert.throws( + () => core.compileModelSync(model, 'CPU', { 'PERFORMANCE_HINT': tput }), + /Cannot convert to ov::Any/ + ); + }); - it('compileModelSync(model) throws if the number of arguments is invalid', () => { - const expectedMsg = ("'compileModelSync' method called with incorrect parameters.\n" + + it('compileModelSync(model) throws if the number of arguments is invalid', () => { + const expectedMsg = ('\'compileModelSync\' method called with incorrect parameters.\n' + 'Provided signature: (object) \n' + 'Allowed signatures:\n' + '- (string, string)\n' + @@ -105,228 +110,230 @@ describe('Core.compileModelSync()', () => { '- (string, string, object)\n' + '- (Model, string, object)\n').replace(/[()]/g, '\\$&'); - assert.throws( - () => core.compileModelSync(model), - new RegExp(expectedMsg), - ); - }); -}); - -describe('Core.compileModel()', () => { - const tput = { 'PERFORMANCE_HINT': 'THROUGHPUT' }; - - it('compileModel(model:Model, deviceName: string, config: {}) ', () => { - core.compileModel(model, 'CPU', tput).then(cm => { - assert.deepStrictEqual(cm.output(0).shape, [1, 10]); + assert.throws( + () => core.compileModelSync(model), + new RegExp(expectedMsg), + ); }); - }); - it('compileModel(model:model_path, deviceName: string, config: {}) ', () => { - core.compileModel(testXml, 'CPU', tput).then(cm => { - assert.equal(cm.inputs.length, 1); - }); + describe('Core.compileModel()', () => { + const tput = { 'PERFORMANCE_HINT': 'THROUGHPUT' }; - }); + it('compileModel(model:Model, deviceName: string, config: {}) ', () => { + core.compileModel(model, 'CPU', tput).then(cm => { + assert.deepStrictEqual(cm.output(0).shape, [1, 10]); + }); - it('compileModel(model:model_path, deviceName: string) ', () => { - core.compileModel(testXml, 'CPU').then(cm => { - assert.deepStrictEqual(cm.output(0).shape, [1, 10]); }); - }); - - it('compileModel(model, device, config) throws when config isn\'t an object', () => { - assert.throws( - () => core.compileModel(model, 'CPU', 'string').then(), - "Argument #3 must be an Object." - ); - }); - - it('compileModel(model, device, config) throws when config value is not a string', () => { - assert.throws( - () => core.compileModel(model, 'CPU', { 'PERFORMANCE_HINT': tput }).then(), - /Cannot convert to ov::Any/ - ); - }); - - it('compileModel(model) throws if the number of arguments is invalid', () => { - assert.throws( - () => core.compileModel(model).then(), - /Invalid number of arguments/ - ); - }); - -}); + it('compileModel(model:model_path, deviceName: string, config: {}) ', () => { + core.compileModel(testXml, 'CPU', tput).then(cm => { + assert.equal(cm.inputs.length, 1); + }); -describe('Output class', () => { + }); - it('Output type', () => { - assert.ok(model.output() instanceof ov.Output); - }); + it('compileModel(model:model_path, deviceName: string) ', () => { + core.compileModel(testXml, 'CPU').then(cm => { + assert.deepStrictEqual(cm.output(0).shape, [1, 10]); + }); - it('ConstOutput type', () => { - assert.ok(compiledModel.output() instanceof ov.ConstOutput); - }); + }); - modelLike.forEach(([obj]) => { - it('Output getters and properties', () => { - assert.strictEqual(obj.outputs.length, 1); - // tests for an obj with one output - assert.strictEqual(obj.output().toString(), 'fc_out'); - assert.strictEqual(obj.output(0).toString(), 'fc_out'); - assert.strictEqual(obj.output('fc_out').toString(), 'fc_out'); - assert.deepStrictEqual(obj.output(0).shape, [1, 10]); - assert.deepStrictEqual(obj.output(0).getShape(), [1, 10]); - assert.strictEqual(obj.output().getAnyName(), 'fc_out'); - assert.strictEqual(obj.output().anyName, 'fc_out'); + it('compileModel(model, device, config) throws when config isn\'t an object', () => { + assert.throws( + () => core.compileModel(model, 'CPU', 'string').then(), + 'Argument #3 must be an Object.' + ); }); - }); -}); + it('compileModel(model, device, config) throws when config value is not a string', () => { + assert.throws( + () => core.compileModel(model, 'CPU', { 'PERFORMANCE_HINT': tput }).then(), + /Cannot convert to ov::Any/ + ); + }); -describe('Input class for ov::Input', () => { + it('compileModel(model) throws if the number of arguments is invalid', () => { + assert.throws( + () => core.compileModel(model).then(), + /Invalid number of arguments/ + ); + }); - it('Output type', () => { - assert.ok(model.input() instanceof ov.Output); }); - it('ConstOutput type', () => { - assert.ok(compiledModel.input() instanceof ov.ConstOutput); - }); + describe('Output class', () => { - modelLike.forEach(([obj]) => { - it('input() is typeof object', () => { - assert.strictEqual(typeof obj.input(), 'object'); + it('Output type', () => { + assert.ok(model.output() instanceof ov.Output); }); - it('inputs property', () => { - assert.strictEqual(obj.inputs.length, 1); + it('ConstOutput type', () => { + assert.ok(compiledModel.output() instanceof ov.ConstOutput); }); - it('input().toString()', () => { - assert.strictEqual(obj.input().toString(), 'data'); + modelLike.forEach(([obj]) => { + it('Output getters and properties', () => { + assert.strictEqual(obj.outputs.length, 1); + // tests for an obj with one output + assert.strictEqual(obj.output().toString(), 'fc_out'); + assert.strictEqual(obj.output(0).toString(), 'fc_out'); + assert.strictEqual(obj.output('fc_out').toString(), 'fc_out'); + assert.deepStrictEqual(obj.output(0).shape, [1, 10]); + assert.deepStrictEqual(obj.output(0).getShape(), [1, 10]); + assert.strictEqual(obj.output().getAnyName(), 'fc_out'); + assert.strictEqual(obj.output().anyName, 'fc_out'); + }); }); - it('input(idx: number).ToString() method', () => { - assert.strictEqual(obj.input(0).toString(), 'data'); - }); + }); - it('input(tensorName: string).ToString() method', () => { - assert.strictEqual(obj.input('data').toString(), 'data'); - }); + describe('Input class for ov::Input', () => { - it('input().getAnyName() and anyName', () => { - assert.strictEqual(obj.input().getAnyName(), 'data'); - assert.strictEqual(obj.input().anyName, 'data'); + it('Output type', () => { + assert.ok(model.input() instanceof ov.Output); }); - it('input(idx).shape property with dimensions', () => { - assert.deepStrictEqual(obj.input(0).shape, [1, 3, 32, 32]); - assert.deepStrictEqual(obj.input(0).getShape(), [1, 3, 32, 32]); + it('ConstOutput type', () => { + assert.ok(compiledModel.input() instanceof ov.ConstOutput); }); - }); -}); + modelLike.forEach(([obj]) => { + it('input() is typeof object', () => { + assert.strictEqual(typeof obj.input(), 'object'); + }); -describe('Test exportModel()/importModel()', () => { - const userStream = compiledModel.exportModelSync(); - const epsilon = 0.5; - const tensor = Float32Array.from({ length: 3072 }, () => (Math.random() + epsilon)); - const inferRequest = compiledModel.createInferRequest(); - const res1 = inferRequest.infer([tensor]); + it('inputs property', () => { + assert.strictEqual(obj.inputs.length, 1); + }); - it('Test importModelSync(stream, device)', () => { - const newCompiled = core.importModelSync(userStream, 'CPU'); - const newInferRequest = newCompiled.createInferRequest(); - const res2 = newInferRequest.infer([tensor]); + it('input().toString()', () => { + assert.strictEqual(obj.input().toString(), 'data'); + }); - assert.deepStrictEqual(res1['fc_out'].data[0], res2['fc_out'].data[0]); - }); + it('input(idx: number).ToString() method', () => { + assert.strictEqual(obj.input(0).toString(), 'data'); + }); - it('Test importModelSync(stream, device, config)', () => { - const newCompiled = core.importModelSync(userStream, 'CPU', { 'NUM_STREAMS': 1 }); - const newInferRequest = newCompiled.createInferRequest(); - const res2 = newInferRequest.infer([tensor]); + it('input(tensorName: string).ToString() method', () => { + assert.strictEqual(obj.input('data').toString(), 'data'); + }); - assert.deepStrictEqual(res1['fc_out'].data[0], res2['fc_out'].data[0]); - }); + it('input().getAnyName() and anyName', () => { + assert.strictEqual(obj.input().getAnyName(), 'data'); + assert.strictEqual(obj.input().anyName, 'data'); + }); - it('Test importModelSync(stream, device) throws', () => { - assert.throws( - () => core.importModelSync(epsilon, 'CPU'), - /The first argument must be of type Buffer./ - ); - }); + it('input(idx).shape property with dimensions', () => { + assert.deepStrictEqual(obj.input(0).shape, [1, 3, 32, 32]); + assert.deepStrictEqual(obj.input(0).getShape(), [1, 3, 32, 32]); + }); + }); - it('Test importModelSync(stream, device) throws', () => { - assert.throws( - () => core.importModelSync(userStream, tensor), - /The second argument must be of type String./ - ); - }); - it('Test importModelSync(stream, device, config: tensor) throws', () => { - assert.throws( - () => core.importModelSync(userStream, 'CPU', tensor), - /NotFound: Unsupported property 0 by CPU plugin./ - ); }); - it('Test importModelSync(stream, device, config: string) throws', () => { - const testString = 'test'; - assert.throws( - () => core.importModelSync(userStream, 'CPU', testString), - /Passed Napi::Value must be an object./ - ); - }); + describe('Test exportModel()/importModel()', () => { + const userStream = compiledModel.exportModelSync(); + const epsilon = 0.5; + const tensor = Float32Array.from({ length: 3072 }, () => (Math.random() + epsilon)); + const inferRequest = compiledModel.createInferRequest(); + const res1 = inferRequest.infer([tensor]); - it('Test importModelSync(stream, device, config: unsupported property) \ - throws', () => { - const tmpDir = '/tmp'; - assert.throws( - () => core.importModelSync(userStream, 'CPU', { 'CACHE_DIR': tmpDir }), - /Unsupported property CACHE_DIR by CPU plugin./ - ); - }); + it('Test importModelSync(stream, device)', () => { + const newCompiled = core.importModelSync(userStream, 'CPU'); + const newInferRequest = newCompiled.createInferRequest(); + const res2 = newInferRequest.infer([tensor]); - it('Test importModel(stream, device)', () => { - core.importModel(userStream, 'CPU').then(newCompiled => { + assert.deepStrictEqual(res1['fc_out'].data[0], res2['fc_out'].data[0]); + }); + + it('Test importModelSync(stream, device, config)', () => { + const newCompiled = core.importModelSync(userStream, 'CPU', { 'NUM_STREAMS': 1 }); const newInferRequest = newCompiled.createInferRequest(); const res2 = newInferRequest.infer([tensor]); + assert.deepStrictEqual(res1['fc_out'].data[0], res2['fc_out'].data[0]); }); - }); - it('Test importModel(stream, device, config)', () => { - core.importModel(userStream, 'CPU', { 'NUM_STREAMS': 1 }).then( - newCompiled => { + it('Test importModelSync(stream, device) throws', () => { + assert.throws( + () => core.importModelSync(epsilon, 'CPU'), + /The first argument must be of type Buffer./ + ); + }); + + it('Test importModelSync(stream, device) throws', () => { + assert.throws( + () => core.importModelSync(userStream, tensor), + /The second argument must be of type String./ + ); + }); + it('Test importModelSync(stream, device, config: tensor) throws', () => { + assert.throws( + () => core.importModelSync(userStream, 'CPU', tensor), + /NotFound: Unsupported property 0 by CPU plugin./ + ); + }); + + it('Test importModelSync(stream, device, config: string) throws', () => { + const testString = 'test'; + assert.throws( + () => core.importModelSync(userStream, 'CPU', testString), + /Passed Napi::Value must be an object./ + ); + }); + + it('Test importModelSync(stream, device, config: unsupported property) \ + throws', () => { + const tmpDir = '/tmp'; + assert.throws( + () => core.importModelSync(userStream, 'CPU', { 'CACHE_DIR': tmpDir }), + /Unsupported property CACHE_DIR by CPU plugin./ + ); + }); + + it('Test importModel(stream, device)', () => { + core.importModel(userStream, 'CPU').then(newCompiled => { const newInferRequest = newCompiled.createInferRequest(); const res2 = newInferRequest.infer([tensor]); - assert.deepStrictEqual(res1['fc_out'].data[0], res2['fc_out'].data[0]); }); - }); + }); - it('Test importModel(stream, device) throws', () => { - assert.throws( - () => core.importModel(epsilon, 'CPU').then(), - /'importModel' method called with incorrect parameters./ - ); - }); + it('Test importModel(stream, device, config)', () => { + core.importModel(userStream, 'CPU', { 'NUM_STREAMS': 1 }).then( + newCompiled => { + const newInferRequest = newCompiled.createInferRequest(); + const res2 = newInferRequest.infer([tensor]); - it('Test importModel(stream, device) throws', () => { - assert.throws( - () => core.importModel(userStream, tensor).then(), - /'importModel' method called with incorrect parameters./ - ); - }); + assert.deepStrictEqual(res1['fc_out'].data[0], res2['fc_out'].data[0]); + }); + }); + + it('Test importModel(stream, device) throws', () => { + assert.throws( + () => core.importModel(epsilon, 'CPU').then(), + /'importModel' method called with incorrect parameters./ + ); + }); + + it('Test importModel(stream, device) throws', () => { + assert.throws( + () => core.importModel(userStream, tensor).then(), + /'importModel' method called with incorrect parameters./ + ); + }); + + it('Test importModel(stream, device, config: string) throws', () => { + const testString = 'test'; + assert.throws( + () => core.importModel(userStream, 'CPU', testString).then(), + /'importModel' method called with incorrect parameters./ + ); + }); - it('Test importModel(stream, device, config: string) throws', () => { - const testString = 'test'; - assert.throws( - () => core.importModel(userStream, 'CPU', testString).then(), - /'importModel' method called with incorrect parameters./ - ); }); }); diff --git a/src/bindings/js/node/tests/unit/compiled_model.test.js b/src/bindings/js/node/tests/unit/compiled_model.test.js index 6b33296131b04d..2d1169b0783e2e 100644 --- a/src/bindings/js/node/tests/unit/compiled_model.test.js +++ b/src/bindings/js/node/tests/unit/compiled_model.test.js @@ -4,17 +4,21 @@ const { addon: ov } = require('../..'); const assert = require('assert'); -const { describe, it } = require('node:test'); -const { getModelPath } = require('./utils.js'); +const { describe, it, before } = require('node:test'); +const { testModels, getModelPath, isModelAvailable } = require('./utils.js'); -const testXml = getModelPath().xml; -const core = new ov.Core(); -const properties = { - "AUTO_BATCH_TIMEOUT": '1' -}; -const compiledModel = core.compileModelSync(testXml, 'BATCH:CPU', properties); +describe('ov.CompiledModel tests', () => { -describe('setProperty() / getProperty()', () => { + before(async () => { + await isModelAvailable(testModels.testModelFP32); + }); + + const testXml = getModelPath().xml; + const core = new ov.Core(); + const properties = { + 'AUTO_BATCH_TIMEOUT': '1', + }; + const compiledModel = core.compileModelSync(testXml, 'BATCH:CPU', properties); describe('getProperty()', () => { it('returns the value of property from compiled model', () => { @@ -26,7 +30,7 @@ describe('setProperty() / getProperty()', () => { /'getProperty' method called with incorrect parameters/ ); }); - it('throws an error when called with property name that does not exists', ()=>{ + it('throws an error when called with property name that does not exists', () => { assert.throws( () => compiledModel.getProperty('PROPERTY_THAT_DOES_NOT_EXIST') ); @@ -35,10 +39,10 @@ describe('setProperty() / getProperty()', () => { describe('setProperty()', () => { it('sets a properties for compiled model', () => { - properties["AUTO_BATCH_TIMEOUT"] = '1000'; + properties['AUTO_BATCH_TIMEOUT'] = '1000'; assert.doesNotThrow(() => compiledModel.setProperty(properties)); }); - + it('throws an error when called without an object argument', () => { assert.throws( () => compiledModel.setProperty(), @@ -47,30 +51,30 @@ describe('setProperty() / getProperty()', () => { }); it('throws an error when called with wrong argument', () => { assert.throws( - () => compiledModel.setProperty(123), - /'setProperty' method called with incorrect parameters/ + () => compiledModel.setProperty(123), + /'setProperty' method called with incorrect parameters/ ); }); - + it('throws an error when called with multiple arguments', () => { assert.throws( - () => compiledModel.setProperty({"PERFORMANCE_HINT": "THROUGHPUT"}, {"NUM_STREAMS": "AUTO"}), + () => compiledModel.setProperty({'PERFORMANCE_HINT': 'THROUGHPUT'}, {'NUM_STREAMS': 'AUTO'}), /'setProperty' method called with incorrect parameters/ ); }); - + it('returns the set property of the compiled model', () => { - properties["AUTO_BATCH_TIMEOUT"] = '123'; + properties['AUTO_BATCH_TIMEOUT'] = '123'; compiledModel.setProperty(properties); assert.strictEqual(compiledModel.getProperty('AUTO_BATCH_TIMEOUT'), 123); }); - + it('retains the last set property when set multiple times', () => { - compiledModel.setProperty({"AUTO_BATCH_TIMEOUT": '321'}); + compiledModel.setProperty({'AUTO_BATCH_TIMEOUT': '321'}); compiledModel.setProperty({'AUTO_BATCH_TIMEOUT': '132'}); assert.strictEqual(compiledModel.getProperty('AUTO_BATCH_TIMEOUT'), 132); }); - + it('allows to pass empty object', () => { assert.doesNotThrow(() => compiledModel.setProperty({})); }); diff --git a/src/bindings/js/node/tests/unit/core.test.js b/src/bindings/js/node/tests/unit/core.test.js index 2c70428b66b7c1..69d8720d97a380 100644 --- a/src/bindings/js/node/tests/unit/core.test.js +++ b/src/bindings/js/node/tests/unit/core.test.js @@ -6,64 +6,67 @@ const { addon: ov } = require('../..'); const assert = require('assert'); const { describe, it } = require('node:test'); -const core = new ov.Core(); +describe('ov.Core tests', () => { -it('Core.setProperty()', () => { - const tmpDir = '/tmp'; + const core = new ov.Core(); - core.setProperty({ 'CACHE_DIR': tmpDir }); + it('Core.setProperty()', () => { + const tmpDir = '/tmp'; - const cacheDir = core.getProperty('CACHE_DIR'); + core.setProperty({ 'CACHE_DIR': tmpDir }); - assert.equal(cacheDir, tmpDir); -}); + const cacheDir = core.getProperty('CACHE_DIR'); -it('Core.setProperty(\'CPU\')', () => { - const tmpDir = '/tmp'; + assert.equal(cacheDir, tmpDir); + }); - core.setProperty('CPU', { 'CACHE_DIR': tmpDir }); + it('Core.setProperty(\'CPU\')', () => { + const tmpDir = '/tmp'; - const cacheDir = core.getProperty('CPU', 'CACHE_DIR'); + core.setProperty('CPU', { 'CACHE_DIR': tmpDir }); - assert.equal(cacheDir, tmpDir); -}); + const cacheDir = core.getProperty('CPU', 'CACHE_DIR'); -it('Core.getProperty(\'CPU\', \'SUPPORTED_PROPERTIES\') is Array', () => { - const supportedPropertiesArray = core.getProperty('CPU', 'SUPPORTED_PROPERTIES'); + assert.equal(cacheDir, tmpDir); + }); - assert.ok(Array.isArray(supportedPropertiesArray)); -}); + it('Core.getProperty(\'CPU\', \'SUPPORTED_PROPERTIES\') is Array', () => { + const supportedPropertiesArray = core.getProperty('CPU', 'SUPPORTED_PROPERTIES'); -it('Core.setProperty(\'CPU\', { \'NUM_STREAMS\': 5 })', () => { - const streams = 5; + assert.ok(Array.isArray(supportedPropertiesArray)); + }); - core.setProperty('CPU', { 'NUM_STREAMS': streams }); - const result = core.getProperty('CPU', 'NUM_STREAMS'); + it('Core.setProperty(\'CPU\', { \'NUM_STREAMS\': 5 })', () => { + const streams = 5; - assert.equal(result, streams); -}); + core.setProperty('CPU', { 'NUM_STREAMS': streams }); + const result = core.getProperty('CPU', 'NUM_STREAMS'); -it('Core.setProperty(\'CPU\', { \'INFERENCE_NUM_THREADS\': 3 })', () => { - const threads = 3; + assert.equal(result, streams); + }); - core.setProperty('CPU', { 'INFERENCE_NUM_THREADS': threads }); - const result = core.getProperty('CPU', 'INFERENCE_NUM_THREADS'); + it('Core.setProperty(\'CPU\', { \'INFERENCE_NUM_THREADS\': 3 })', () => { + const threads = 3; - assert.equal(result, threads); -}); + core.setProperty('CPU', { 'INFERENCE_NUM_THREADS': threads }); + const result = core.getProperty('CPU', 'INFERENCE_NUM_THREADS'); -it('Core.addExtension() with empty parameters', () => { - assert.throws( - () => core.addExtension(), - /addExtension method applies one argument of string type/ - ); -}); + assert.equal(result, threads); + }); + + it('Core.addExtension() with empty parameters', () => { + assert.throws( + () => core.addExtension(), + /addExtension method applies one argument of string type/ + ); + }); -it('Core.addExtension(\'not_exists\') with non-existed library', () => { - const notExistsExt = 'not_exists'; + it('Core.addExtension(\'not_exists\') with non-existed library', () => { + const notExistsExt = 'not_exists'; - assert.throws( - () => core.addExtension(notExistsExt), - /Cannot load library 'not_exists'/ - ); + assert.throws( + () => core.addExtension(notExistsExt), + /Cannot load library 'not_exists'/ + ); + }); }); diff --git a/src/bindings/js/node/tests/unit/infer_request.test.js b/src/bindings/js/node/tests/unit/infer_request.test.js index bdc54fc4f4d086..8dc16cd195430a 100644 --- a/src/bindings/js/node/tests/unit/infer_request.test.js +++ b/src/bindings/js/node/tests/unit/infer_request.test.js @@ -4,277 +4,286 @@ const { addon: ov } = require('../..'); const assert = require('assert'); -const { describe, it } = require('node:test'); -const { getModelPath } = require('./utils.js'); +const { describe, it, before } = require('node:test'); +const { testModels, isModelAvailable, getModelPath } = require('./utils.js'); const epsilon = 0.5; // To avoid very small numbers const testXml = getModelPath().xml; -const core = new ov.Core(); -const model = core.readModelSync(testXml); -const compiledModel = core.compileModelSync(model, 'CPU'); - -const inferRequest = compiledModel.createInferRequest(); -const inferRequestAsync = compiledModel.createInferRequest(); - -const tensorData = Float32Array.from({ length: 3072 }, () => (Math.random() + epsilon)); -const tensor = new ov.Tensor( - ov.element.f32, - [1, 3, 32, 32], - tensorData, -); -const resTensor = new ov.Tensor( - ov.element.f32, - [1, 10], - tensorData.slice(-10), -); - -const tensorLike = [[tensor], - [tensorData]]; - -describe('InferRequest', () => { - - tensorLike.forEach(([tl]) => { - const result = inferRequest.infer({ data: tl }); - const label = tl instanceof Float32Array ? 'TypedArray[]' : 'Tensor[]'; - it(`Test infer(inputData: { [inputName: string]: ${label} })`, () => { - assert.deepStrictEqual(Object.keys(result), ['fc_out']); - assert.deepStrictEqual(result['fc_out'].data.length, 10); - }); - }); - tensorLike.forEach(([tl]) => { - const result = inferRequest.infer([tl]); - const label = tl instanceof Float32Array ? 'TypedArray[]' : 'Tensor[]'; - it(`Test infer(inputData: ${label})`, () => { - assert.deepStrictEqual(Object.keys(result), ['fc_out']); - assert.deepStrictEqual(result['fc_out'].data.length, 10); - }); +describe('ov.InferRequest tests', () => { + before(async () => { + await isModelAvailable(testModels.testModelFP32); }); - it('Test infer(TypedArray) throws', () => { - assert.throws( - () => inferRequest.infer(tensorData), - {message: /TypedArray cannot be passed directly into infer\(\) method./}); - }); + const core = new ov.Core(); + const model = core.readModelSync(testXml); + const compiledModel = core.compileModelSync(model, 'CPU'); - const buffer = new ArrayBuffer(tensorData.length); - const inputMessagePairs = [ - ['string', 'Cannot create a tensor from the passed Napi::Value.'], - [tensorData.slice(-10), 'Memory allocated using shape and element::type mismatch passed data\'s size'], - [new Float32Array(buffer, 4), 'TypedArray.byteOffset has to be equal to zero.'], - [{}, /Invalid argument/], // Test for object that is not Tensor - ]; + const inferRequest = compiledModel.createInferRequest(); + const inferRequestAsync = compiledModel.createInferRequest(); + + const tensorData = Float32Array.from({ length: 3072 }, () => (Math.random() + epsilon)); + const tensor = new ov.Tensor( + ov.element.f32, + [1, 3, 32, 32], + tensorData, + ); + const resTensor = new ov.Tensor( + ov.element.f32, + [1, 10], + tensorData.slice(-10), + ); + + const tensorLike = [[tensor], + [tensorData]]; + + describe('InferRequest', () => { + + tensorLike.forEach(([tl]) => { + const result = inferRequest.infer({ data: tl }); + const label = tl instanceof Float32Array ? 'TypedArray[]' : 'Tensor[]'; + it(`Test infer(inputData: { [inputName: string]: ${label} })`, () => { + assert.deepStrictEqual(Object.keys(result), ['fc_out']); + assert.deepStrictEqual(result['fc_out'].data.length, 10); + }); + }); + + tensorLike.forEach(([tl]) => { + const result = inferRequest.infer([tl]); + const label = tl instanceof Float32Array ? 'TypedArray[]' : 'Tensor[]'; + it(`Test infer(inputData: ${label})`, () => { + assert.deepStrictEqual(Object.keys(result), ['fc_out']); + assert.deepStrictEqual(result['fc_out'].data.length, 10); + }); - inputMessagePairs.forEach( ([tl, msg]) => { - it(`Test infer([data]) throws ${msg}`, () => { - assert.throws( - () => inferRequest.infer([tl]), - {message: new RegExp(msg)}); }); - it(`Test infer({ data: tl}) throws ${msg}`, () => { + + it('Test infer(TypedArray) throws', () => { assert.throws( - () => inferRequest.infer({data: tl}), - {message: new RegExp(msg)}); + () => inferRequest.infer(tensorData), + {message: /TypedArray cannot be passed directly into infer\(\) method./}); }); - }); - it('Test inferAsync(inputData: { [inputName: string]: Tensor })', () => { - inferRequestAsync.inferAsync({ data: tensor }).then(result => { - assert.ok(result['fc_out'] instanceof ov.Tensor); - assert.deepStrictEqual(Object.keys(result), ['fc_out']); - assert.deepStrictEqual(result['fc_out'].data.length, 10);} - ); - }); + const buffer = new ArrayBuffer(tensorData.length); + const inputMessagePairs = [ + ['string', 'Cannot create a tensor from the passed Napi::Value.'], + [tensorData.slice(-10), 'Memory allocated using shape and element::type mismatch passed data\'s size'], + [new Float32Array(buffer, 4), 'TypedArray.byteOffset has to be equal to zero.'], + [{}, /Invalid argument/], // Test for object that is not Tensor + ]; + + inputMessagePairs.forEach( ([tl, msg]) => { + it(`Test infer([data]) throws ${msg}`, () => { + assert.throws( + () => inferRequest.infer([tl]), + {message: new RegExp(msg)}); + }); + it(`Test infer({ data: tl}) throws ${msg}`, () => { + assert.throws( + () => inferRequest.infer({data: tl}), + {message: new RegExp(msg)}); + }); + }); - it('Test inferAsync(inputData: Tensor[])', () => { - inferRequestAsync.inferAsync([ tensor ]).then(result => { - assert.ok(result['fc_out'] instanceof ov.Tensor); - assert.deepStrictEqual(Object.keys(result), ['fc_out']); - assert.deepStrictEqual(result['fc_out'].data.length, 10); + it('Test inferAsync(inputData: { [inputName: string]: Tensor })', () => { + inferRequestAsync.inferAsync({ data: tensor }).then(result => { + assert.ok(result['fc_out'] instanceof ov.Tensor); + assert.deepStrictEqual(Object.keys(result), ['fc_out']); + assert.deepStrictEqual(result['fc_out'].data.length, 10);} + ); }); - }); - it('Test inferAsync([data]) throws: Cannot create a tensor from the passed Napi::Value.', () => { - assert.throws( - () => inferRequestAsync.inferAsync(['string']).then(), - /Cannot create a tensor from the passed Napi::Value./ - ); - }); + it('Test inferAsync(inputData: Tensor[])', () => { + inferRequestAsync.inferAsync([ tensor ]).then(result => { + assert.ok(result['fc_out'] instanceof ov.Tensor); + assert.deepStrictEqual(Object.keys(result), ['fc_out']); + assert.deepStrictEqual(result['fc_out'].data.length, 10); + }); + }); - it('Test inferAsync({ data: "string"}) throws: Cannot create a tensor from the passed Napi::Value.', () => { - assert.throws( - () => inferRequestAsync.inferAsync({data: 'string'}).then(), - /Cannot create a tensor from the passed Napi::Value./ - ); - }); + it('Test inferAsync([data]) throws: Cannot create a tensor from the passed Napi::Value.', () => { + assert.throws( + () => inferRequestAsync.inferAsync(['string']).then(), + /Cannot create a tensor from the passed Napi::Value./ + ); + }); - it('Test setInputTensor(tensor)', () => { - inferRequest.setInputTensor(tensor); - const t1 = inferRequest.getInputTensor(); - assert.deepStrictEqual(tensor.data[0], t1.data[0]); - }); + it('Test inferAsync({ data: "string"}) throws: Cannot create a tensor from the passed Napi::Value.', () => { + assert.throws( + () => inferRequestAsync.inferAsync({data: 'string'}).then(), + /Cannot create a tensor from the passed Napi::Value./ + ); + }); - it('Test setInputTensor(object) throws when passed object is not a Tensor.', () => { - assert.throws( - () => inferRequest.setInputTensor({}), - {message: /Argument #[0-9]+ must be a Tensor./} - ); - }); + it('Test setInputTensor(tensor)', () => { + inferRequest.setInputTensor(tensor); + const t1 = inferRequest.getInputTensor(); + assert.deepStrictEqual(tensor.data[0], t1.data[0]); + }); - it('Test setInputTensor(idx, tensor)', () => { - inferRequest.setInputTensor(0, tensor); - const t1 = inferRequest.getInputTensor(); - assert.deepStrictEqual(tensor.data[0], t1.data[0]); - }); + it('Test setInputTensor(object) throws when passed object is not a Tensor.', () => { + assert.throws( + () => inferRequest.setInputTensor({}), + {message: /Argument #[0-9]+ must be a Tensor./} + ); + }); - it('Test setInputTensor(idx, tensor) throws', () => { - const testIdx = 10; - assert.throws ( - () => inferRequest.setInputTensor(testIdx, tensor), - {message: /Input port for index [0-9]+ was not found!/} - ); - }); + it('Test setInputTensor(idx, tensor)', () => { + inferRequest.setInputTensor(0, tensor); + const t1 = inferRequest.getInputTensor(); + assert.deepStrictEqual(tensor.data[0], t1.data[0]); + }); - it('Test setInputTensor(idx, object) throws when passed object is not a Tensor.', () => { - assert.throws( - () => inferRequest.setInputTensor(0, {}), - {message: /Argument #[0-9]+ must be a Tensor./} - ); - }); + it('Test setInputTensor(idx, tensor) throws', () => { + const testIdx = 10; + assert.throws ( + () => inferRequest.setInputTensor(testIdx, tensor), + {message: /Input port for index [0-9]+ was not found!/} + ); + }); - it('Test setInputTensor(tensor, tensor) throws', () => { - assert.throws( - () => inferRequest.setInputTensor(resTensor, tensor), - {message: / invalid argument./} - ); - }); + it('Test setInputTensor(idx, object) throws when passed object is not a Tensor.', () => { + assert.throws( + () => inferRequest.setInputTensor(0, {}), + {message: /Argument #[0-9]+ must be a Tensor./} + ); + }); - it('Test setOutputTensor(tensor)', () => { - inferRequest.setOutputTensor(resTensor); - const res2 = inferRequest.getOutputTensor(); - assert.deepStrictEqual(resTensor.data[0], res2.data[0]); - }); + it('Test setInputTensor(tensor, tensor) throws', () => { + assert.throws( + () => inferRequest.setInputTensor(resTensor, tensor), + {message: / invalid argument./} + ); + }); - it('Test setOutputTensor(object) throws when passed object is not a Tensor.', () => { - assert.throws( - () => inferRequest.setOutputTensor({}), - {message: /Argument #[0-9]+ must be a Tensor./} - ); - }); + it('Test setOutputTensor(tensor)', () => { + inferRequest.setOutputTensor(resTensor); + const res2 = inferRequest.getOutputTensor(); + assert.deepStrictEqual(resTensor.data[0], res2.data[0]); + }); - it('Test setOutputTensor(idx, tensor) throws', () => { - const testIdx = 10; - assert.throws ( - () => inferRequest.setOutputTensor(testIdx, tensor), - {message: /Output port for index [0-9]+ was not found!/} - ); - }); + it('Test setOutputTensor(object) throws when passed object is not a Tensor.', () => { + assert.throws( + () => inferRequest.setOutputTensor({}), + {message: /Argument #[0-9]+ must be a Tensor./} + ); + }); - it('Test setOutputTensor(idx, tensor)', () => { - inferRequest.setOutputTensor(0, resTensor); - const res2 = inferRequest.getOutputTensor(); - assert.deepStrictEqual(resTensor.data[0], res2.data[0]); - }); + it('Test setOutputTensor(idx, tensor) throws', () => { + const testIdx = 10; + assert.throws ( + () => inferRequest.setOutputTensor(testIdx, tensor), + {message: /Output port for index [0-9]+ was not found!/} + ); + }); - it('Test setOutputTensor(idx, tensor) throws when passed object is not a Tensor.', () => { - assert.throws( - () => inferRequest.setOutputTensor(0, {}), - {message: /Argument #[0-9]+ must be a Tensor./} - ); - }); + it('Test setOutputTensor(idx, tensor)', () => { + inferRequest.setOutputTensor(0, resTensor); + const res2 = inferRequest.getOutputTensor(); + assert.deepStrictEqual(resTensor.data[0], res2.data[0]); + }); - it('Test setOutputTensor() - pass two tensors', () => { - assert.throws( - () => inferRequest.setOutputTensor(resTensor, tensor), - {message: / invalid argument./}); - }); + it('Test setOutputTensor(idx, tensor) throws when passed object is not a Tensor.', () => { + assert.throws( + () => inferRequest.setOutputTensor(0, {}), + {message: /Argument #[0-9]+ must be a Tensor./} + ); + }); - it('Test setTensor(string, tensor)', () => { - inferRequest.setTensor('fc_out', resTensor); - const res2 = inferRequest.getTensor('fc_out'); - assert.ok(res2 instanceof ov.Tensor); - assert.deepStrictEqual(resTensor.data[0], res2.data[0]); - }); + it('Test setOutputTensor() - pass two tensors', () => { + assert.throws( + () => inferRequest.setOutputTensor(resTensor, tensor), + {message: / invalid argument./}); + }); - it('Test setTensor(string, object) - throws', () => { - const testName = 'testName'; - assert.throws( - () => inferRequest.setTensor(testName, tensor), - {message: /Port for tensor name testName was not found./}); - }); + it('Test setTensor(string, tensor)', () => { + inferRequest.setTensor('fc_out', resTensor); + const res2 = inferRequest.getTensor('fc_out'); + assert.ok(res2 instanceof ov.Tensor); + assert.deepStrictEqual(resTensor.data[0], res2.data[0]); + }); - it('Test setTensor(string, object) - throws', () => { - assert.throws( - () => inferRequest.setTensor('fc_out', {}), - {message: /Argument #[0-9]+ must be a Tensor./}); - }); + it('Test setTensor(string, object) - throws', () => { + const testName = 'testName'; + assert.throws( + () => inferRequest.setTensor(testName, tensor), + {message: /Port for tensor name testName was not found./}); + }); - it('Test setTensor(string, tensor) - pass one arg', () => { - assert.throws( - () => inferRequest.setTensor('fc_out'), - {message: / invalid argument./} - ); - }); + it('Test setTensor(string, object) - throws', () => { + assert.throws( + () => inferRequest.setTensor('fc_out', {}), + {message: /Argument #[0-9]+ must be a Tensor./}); + }); - it('Test setTensor(string, tensor) - pass args in wrong order', () => { - assert.throws( - () => inferRequest.setTensor(resTensor, 'fc_out'), - {message: / invalid argument./} - ); - }); + it('Test setTensor(string, tensor) - pass one arg', () => { + assert.throws( + () => inferRequest.setTensor('fc_out'), + {message: / invalid argument./} + ); + }); - it('Test setTensor(string, tensor) - pass number as first arg', () => { - assert.throws( - () => inferRequest.setTensor(123, 'fc_out'), - {message: / invalid argument/} - ); - }); + it('Test setTensor(string, tensor) - pass args in wrong order', () => { + assert.throws( + () => inferRequest.setTensor(resTensor, 'fc_out'), + {message: / invalid argument./} + ); + }); - const irGetters = compiledModel.createInferRequest(); - irGetters.setInputTensor(tensor); - irGetters.infer(); + it('Test setTensor(string, tensor) - pass number as first arg', () => { + assert.throws( + () => inferRequest.setTensor(123, 'fc_out'), + {message: / invalid argument/} + ); + }); - it('Test getTensor(tensorName)', () => { - const t1 = irGetters.getTensor('data'); - assert.ok(t1 instanceof ov.Tensor); - assert.deepStrictEqual(tensor.data[0], t1.data[0]); - }); + const irGetters = compiledModel.createInferRequest(); + irGetters.setInputTensor(tensor); + irGetters.infer(); - it('Test getTensor(Output)', () => { - const input = irGetters.getCompiledModel().input(); - const t1 = irGetters.getTensor(input); - assert.ok(t1 instanceof ov.Tensor); - assert.deepStrictEqual(tensor.data[0], t1.data[0]); - }); + it('Test getTensor(tensorName)', () => { + const t1 = irGetters.getTensor('data'); + assert.ok(t1 instanceof ov.Tensor); + assert.deepStrictEqual(tensor.data[0], t1.data[0]); + }); - it('Test getInputTensor()', () => { - const t1 = irGetters.getInputTensor(); - assert.ok(t1 instanceof ov.Tensor); - assert.deepStrictEqual(tensor.data[0], t1.data[0]); - }); + it('Test getTensor(Output)', () => { + const input = irGetters.getCompiledModel().input(); + const t1 = irGetters.getTensor(input); + assert.ok(t1 instanceof ov.Tensor); + assert.deepStrictEqual(tensor.data[0], t1.data[0]); + }); - it('Test getInputTensor(idx)', () => { - const t1 = irGetters.getInputTensor(0); - assert.ok(t1 instanceof ov.Tensor); - assert.deepStrictEqual(tensor.data[0], t1.data[0]); - }); + it('Test getInputTensor()', () => { + const t1 = irGetters.getInputTensor(); + assert.ok(t1 instanceof ov.Tensor); + assert.deepStrictEqual(tensor.data[0], t1.data[0]); + }); - it('Test getOutputTensor(idx?)', () => { - const res1 = irGetters.getOutputTensor(); - const res2 = irGetters.getOutputTensor(0); - assert.ok(res1 instanceof ov.Tensor); - assert.ok(res2 instanceof ov.Tensor); - assert.deepStrictEqual(res1.data[0], res2.data[0]); - }); + it('Test getInputTensor(idx)', () => { + const t1 = irGetters.getInputTensor(0); + assert.ok(t1 instanceof ov.Tensor); + assert.deepStrictEqual(tensor.data[0], t1.data[0]); + }); + + it('Test getOutputTensor(idx?)', () => { + const res1 = irGetters.getOutputTensor(); + const res2 = irGetters.getOutputTensor(0); + assert.ok(res1 instanceof ov.Tensor); + assert.ok(res2 instanceof ov.Tensor); + assert.deepStrictEqual(res1.data[0], res2.data[0]); + }); - it('Test getCompiledModel()', () => { - const ir = compiledModel.createInferRequest(); - const cm = ir.getCompiledModel(); - assert.ok(cm instanceof ov.CompiledModel); - const ir2 = cm.createInferRequest(); - const res2 = ir2.infer([tensorData]); - const res1 = ir.infer([tensorData]); - assert.deepStrictEqual(res1['fc_out'].data[0], res2['fc_out'].data[0]); + it('Test getCompiledModel()', () => { + const ir = compiledModel.createInferRequest(); + const cm = ir.getCompiledModel(); + assert.ok(cm instanceof ov.CompiledModel); + const ir2 = cm.createInferRequest(); + const res2 = ir2.infer([tensorData]); + const res1 = ir.infer([tensorData]); + assert.deepStrictEqual(res1['fc_out'].data[0], res2['fc_out'].data[0]); + }); }); + }); diff --git a/src/bindings/js/node/tests/unit/model.test.js b/src/bindings/js/node/tests/unit/model.test.js index 7728f13a25dce9..e374c0b3ab3cad 100644 --- a/src/bindings/js/node/tests/unit/model.test.js +++ b/src/bindings/js/node/tests/unit/model.test.js @@ -4,45 +4,51 @@ const { addon: ov } = require('../..'); const assert = require('assert'); -const { describe, it } = require('node:test'); -const { getModelPath } = require('./utils.js'); -const testXml = getModelPath().xml; -const core = new ov.Core(); -const model = core.readModelSync(testXml); -const clonedModel = model.clone(); - -describe('Node.js Model.isDynamic()', () => { - it('should return a boolean value indicating if the model is dynamic', () => { - const result = model.isDynamic(); - assert.strictEqual( - typeof result, - 'boolean', - 'isDynamic() should return a boolean value' - ); - }); +const { describe, it, before } = require('node:test'); +const { testModels, getModelPath, isModelAvailable } = require('./utils.js'); + +describe('ov.Model tests', () => { - it('should not accept any arguments', () => { - assert.throws( - () => { - model.isDynamic('unexpected argument'); - }, - /^Error: isDynamic\(\) does not accept any arguments\.$/, - 'Expected isDynamic to throw an error when called with arguments' - ); + before(async () => { + await isModelAvailable(testModels.testModelFP32); }); - it('returns false for a static model', () => { - const expectedStatus = false; - assert.strictEqual( - model.isDynamic(), - expectedStatus, - 'Expected isDynamic to return false for a static model' - ); + const testXml = getModelPath().xml; + const core = new ov.Core(); + const model = core.readModelSync(testXml); + const clonedModel = model.clone(); + + describe('Model.isDynamic()', () => { + it('should return a boolean value indicating if the model is dynamic', () => { + const result = model.isDynamic(); + assert.strictEqual( + typeof result, + 'boolean', + 'isDynamic() should return a boolean value' + ); + }); + + it('should not accept any arguments', () => { + assert.throws( + () => { + model.isDynamic('unexpected argument'); + }, + /^Error: isDynamic\(\) does not accept any arguments\.$/, + 'Expected isDynamic to throw an error when called with arguments' + ); + }); + + it('returns false for a static model', () => { + const expectedStatus = false; + assert.strictEqual( + model.isDynamic(), + expectedStatus, + 'Expected isDynamic to return false for a static model' + ); + }); }); -}); -describe('Node.js getFriendlyName() / setFriendlyName()', () => { - describe('getFriendlyName()', () => { + describe('Model.getFriendlyName()', () => { it('returns the unique name of the model if no friendly name is set', () => { const expectedName = 'test_model'; assert.strictEqual(model.getFriendlyName(), expectedName); @@ -54,7 +60,7 @@ describe('Node.js getFriendlyName() / setFriendlyName()', () => { ); }); }); - describe('setFriendlyName()', () => { + describe('Model.setFriendlyName()', () => { it('sets a friendly name for the model', () => { assert.doesNotThrow(() => model.setFriendlyName('MyFriendlyName')); }); @@ -94,84 +100,85 @@ describe('Node.js getFriendlyName() / setFriendlyName()', () => { assert.strictEqual(model.getFriendlyName(), 'Model1'); }); }); -}); -describe('Model.getOutputSize()', () => { + describe('Model.getOutputSize()', () => { - it('should return a number indicating number of outputs for the model', () => { - const result = model.getOutputSize(); - assert.strictEqual(typeof result, 'number', 'getOutputSize() should return a number'); - }); + it('should return a number indicating number of outputs for the model', () => { + const result = model.getOutputSize(); + assert.strictEqual(typeof result, 'number', 'getOutputSize() should return a number'); + }); - it('should not accept any arguments', () => { - assert.throws(() => { - model.getOutputSize('unexpected argument'); - }, /^Error: getOutputSize\(\) does not accept any arguments\.$/, 'Expected getOutputSize to throw an error when called with arguments'); - }); + it('should not accept any arguments', () => { + assert.throws(() => { + model.getOutputSize('unexpected argument'); + }, /^Error: getOutputSize\(\) does not accept any arguments\.$/, 'Expected getOutputSize to throw an error when called with arguments'); + }); - it('should return 1 for the default model', () => { - assert.strictEqual(model.getOutputSize(), 1, 'Expected getOutputSize to return 1 for the default model'); + it('should return 1 for the default model', () => { + assert.strictEqual(model.getOutputSize(), 1, 'Expected getOutputSize to return 1 for the default model'); + }); }); -}); -describe('Model.getOutputElementType()', () => { - it('should return a string for the element type ', () => { - const result = model.getOutputElementType(0); - assert.strictEqual(typeof result, 'string', - 'getOutputElementType() should return a string'); - }); + describe('Model.getOutputElementType()', () => { + it('should return a string for the element type ', () => { + const result = model.getOutputElementType(0); + assert.strictEqual(typeof result, 'string', + 'getOutputElementType() should return a string'); + }); - it('should accept a single integer argument', () => { - assert.throws(() => { - model.getOutputElementType(); - }, /'getOutputElementType' method called with incorrect parameters/, - 'Should throw when called without arguments'); - - assert.throws(() => { - model.getOutputElementType('unexpected argument'); - }, /'getOutputElementType' method called with incorrect parameters/, - 'Should throw on non-number argument'); - - assert.throws(() => { - model.getOutputElementType(0, 1); - }, /'getOutputElementType' method called with incorrect parameters/, - 'Should throw on multiple arguments'); - - assert.throws(() => { - model.getOutputElementType(3.14); - }, /'getOutputElementType' method called with incorrect parameters/, - 'Should throw on non-integer number'); - }); + it('should accept a single integer argument', () => { + assert.throws(() => { + model.getOutputElementType(); + }, /'getOutputElementType' method called with incorrect parameters/, + 'Should throw when called without arguments'); + + assert.throws(() => { + model.getOutputElementType('unexpected argument'); + }, /'getOutputElementType' method called with incorrect parameters/, + 'Should throw on non-number argument'); + + assert.throws(() => { + model.getOutputElementType(0, 1); + }, /'getOutputElementType' method called with incorrect parameters/, + 'Should throw on multiple arguments'); + + assert.throws(() => { + model.getOutputElementType(3.14); + }, /'getOutputElementType' method called with incorrect parameters/, + 'Should throw on non-integer number'); + }); - it('should return a valid element type for the default model', () => { - const elementType = model.getOutputElementType(0); - assert.ok(typeof elementType === 'string' && elementType.length > 0, - `Expected a non-empty string, got ${elementType}`); - }); + it('should return a valid element type for the default model', () => { + const elementType = model.getOutputElementType(0); + assert.ok(typeof elementType === 'string' && elementType.length > 0, + `Expected a non-empty string, got ${elementType}`); + }); - it('should throw an error for out-of-range index', () => { - const outputSize = model.getOutputSize(); - assert.throws( - () => { model.getOutputElementType(outputSize); }, - /^Error: /, - 'Should throw for out-of-range index' - ); + it('should throw an error for out-of-range index', () => { + const outputSize = model.getOutputSize(); + assert.throws( + () => { model.getOutputElementType(outputSize); }, + /^Error: /, + 'Should throw for out-of-range index' + ); + }); }); -}); -describe('Model.clone()', () => { - it('should return an object of type model', () => { - assert.ok(clonedModel instanceof ov.Model, 'clone() should return a model'); - }); + describe('Model.clone()', () => { + it('should return an object of type model', () => { + assert.ok(clonedModel instanceof ov.Model, 'clone() should return a model'); + }); - it('should return a model that is a clone of the calling model', () => { - assert.deepStrictEqual(clonedModel, model, "Cloned Model should be exactly equal to the calling model"); - }); - - it('should not accept any arguments', () => { - assert.throws( - () => model.clone("Unexpected argument").then(), - /'clone' method called with incorrect parameters./ - ); + it('should return a model that is a clone of the calling model', () => { + assert.deepStrictEqual(clonedModel, model, 'Cloned Model should be exactly equal to the calling model'); + }); + + it('should not accept any arguments', () => { + assert.throws( + () => model.clone('Unexpected argument').then(), + /'clone' method called with incorrect parameters./ + ); + }); }); + }); diff --git a/src/bindings/js/node/tests/unit/partial_shape.test.js b/src/bindings/js/node/tests/unit/partial_shape.test.js index 98f7d372ce8191..e46af099f03e27 100644 --- a/src/bindings/js/node/tests/unit/partial_shape.test.js +++ b/src/bindings/js/node/tests/unit/partial_shape.test.js @@ -9,46 +9,46 @@ const { describe, it } = require('node:test'); const staticShape = '1, 3, 224, 224'; const dynamicShape = '?, -1, 1..3, 224'; -describe('PartialShape', () => { - it('Allows create empty shape', () => { +describe('ov.PartialShape tests', () => { + it('allows create empty shape', () => { const partialShape = new ov.PartialShape(); assert.strictEqual(partialShape.toString(), '[]'); }); - it('Should detect static shape', () => { + it('should detect static shape', () => { const partialShape = new ov.PartialShape(staticShape); assert.ok(partialShape.isStatic()); }); - it('Should detect dynamic shape', () => { + it('should detect dynamic shape', () => { const partialShape = new ov.PartialShape(dynamicShape); assert.strictEqual(partialShape.isStatic(), false); }); - it('Should return shape as string for static shape', () => { + it('should return shape as string for static shape', () => { const partialShape = new ov.PartialShape(staticShape); assert.strictEqual(partialShape.toString(), '[1,3,224,224]'); }); - it('Should return shape as string for dynamic shape', () => { + it('should return shape as string for dynamic shape', () => { const partialShape = new ov.PartialShape(dynamicShape); assert.strictEqual(partialShape.toString(), '[?,?,1..3,224]'); }); - it('Should return array with dimensions for dynamic shape', () => { + it('should return array with dimensions for dynamic shape', () => { const partialShape = new ov.PartialShape(staticShape); - assert.deepStrictEqual(partialShape.getDimensions(), [1,3,224,224]); + assert.deepStrictEqual(partialShape.getDimensions(), [1, 3, 224, 224]); }); - it('Should return array with dimensions for dynamic shape', () => { + it('should return array with dimensions for dynamic shape', () => { const partialShape = new ov.PartialShape(dynamicShape); - assert.deepStrictEqual(partialShape.getDimensions(), [-1,-1,[1,3],224]); + assert.deepStrictEqual(partialShape.getDimensions(), [-1, -1, [1, 3], 224]); }); }); diff --git a/src/bindings/js/node/tests/unit/pre_post_processor.test.js b/src/bindings/js/node/tests/unit/pre_post_processor.test.js index a4570ce5fdd6a4..f3068f52581732 100644 --- a/src/bindings/js/node/tests/unit/pre_post_processor.test.js +++ b/src/bindings/js/node/tests/unit/pre_post_processor.test.js @@ -4,161 +4,167 @@ const { addon: ov } = require('../..'); const assert = require('assert'); -const { describe, it } = require('node:test'); -const { getModelPath } = require('./utils.js'); +const { describe, it, before } = require('node:test'); +const { testModels, getModelPath, isModelAvailable } = require('./utils.js'); -const testXml = getModelPath().xml; -const core = new ov.Core(); -const model = core.readModelSync(testXml); +describe('ov.preprocess.PrePostProcessor tests', () => { -describe('PrePostProcess', () => { - - it('input() ', () => { - assert.doesNotThrow(() => - new ov.preprocess.PrePostProcessor(model).input()); + before(async () => { + await isModelAvailable(testModels.testModelFP32); }); - it('input(size_t input_index)', () => { - assert.doesNotThrow(() => - new ov.preprocess.PrePostProcessor(model).input(0)); - }); + const testXml = getModelPath().xml; + const core = new ov.Core(); + const model = core.readModelSync(testXml); - it('input(const std::string& tensor_name)', () => { - assert.doesNotThrow(() => - new ov.preprocess.PrePostProcessor(model).input('data')); - }); + describe('PrePostProcess', () => { - it('output() ', () => { - assert.doesNotThrow(() => - new ov.preprocess.PrePostProcessor(model).output()); - }); + it('input() ', () => { + assert.doesNotThrow(() => + new ov.preprocess.PrePostProcessor(model).input()); + }); - it('output(size_t output_index)', () => { - assert.doesNotThrow(() => - new ov.preprocess.PrePostProcessor(model).output(0)); - }); + it('input(size_t input_index)', () => { + assert.doesNotThrow(() => + new ov.preprocess.PrePostProcessor(model).input(0)); + }); - it('output(const std::string& tensor_name)', () => { - assert.doesNotThrow(() => - new ov.preprocess.PrePostProcessor(model).output('fc_out')); - }); + it('input(const std::string& tensor_name)', () => { + assert.doesNotThrow(() => + new ov.preprocess.PrePostProcessor(model).input('data')); + }); -}); + it('output() ', () => { + assert.doesNotThrow(() => + new ov.preprocess.PrePostProcessor(model).output()); + }); -describe('InputInfo', () => { - it('tensor()', () => { - assert.doesNotThrow(() => - new ov.preprocess.PrePostProcessor(model).input(0).tensor()); - }); + it('output(size_t output_index)', () => { + assert.doesNotThrow(() => + new ov.preprocess.PrePostProcessor(model).output(0)); + }); - it('preprocess()', () => { - assert.doesNotThrow(() => - new ov.preprocess.PrePostProcessor(model).input(0).preprocess()); - }); + it('output(const std::string& tensor_name)', () => { + assert.doesNotThrow(() => + new ov.preprocess.PrePostProcessor(model).output('fc_out')); + }); - it('model()', () => { - assert.doesNotThrow(() => - new ov.preprocess.PrePostProcessor(model).input(0).model()); }); - it('tensor(param) throws', () => { - assert.throws(() => - new ov.preprocess.PrePostProcessor(model).input(0).tensor(0), + describe('InputInfo', () => { + it('tensor()', () => { + assert.doesNotThrow(() => + new ov.preprocess.PrePostProcessor(model).input(0).tensor()); + }); + + it('preprocess()', () => { + assert.doesNotThrow(() => + new ov.preprocess.PrePostProcessor(model).input(0).preprocess()); + }); + + it('model()', () => { + assert.doesNotThrow(() => + new ov.preprocess.PrePostProcessor(model).input(0).model()); + }); + + it('tensor(param) throws', () => { + assert.throws(() => + new ov.preprocess.PrePostProcessor(model).input(0).tensor(0), /Function does not take any parameters./); - }); + }); - it('preprocess(param) throws', () => { - assert.throws(() => - new ov.preprocess.PrePostProcessor(model).input(0).preprocess(0), + it('preprocess(param) throws', () => { + assert.throws(() => + new ov.preprocess.PrePostProcessor(model).input(0).preprocess(0), /Function does not take any parameters./); - }); + }); - it('model(param) throws', () => { - assert.throws(() => - new ov.preprocess.PrePostProcessor(model).input(0).model(0), + it('model(param) throws', () => { + assert.throws(() => + new ov.preprocess.PrePostProcessor(model).input(0).model(0), /Function does not take any parameters./); - }); + }); - it('tensor().setElementType()', () => { - assert.doesNotThrow(() => - new ov.preprocess.PrePostProcessor(model).input(0).tensor().setElementType(ov.element.u8)); - }); + it('tensor().setElementType()', () => { + assert.doesNotThrow(() => + new ov.preprocess.PrePostProcessor(model).input(0).tensor().setElementType(ov.element.u8)); + }); - it('tensor().setElementType() throws', () => { - assert.throws(() => - new ov.preprocess.PrePostProcessor(model).input(0).tensor().setElementType(), + it('tensor().setElementType() throws', () => { + assert.throws(() => + new ov.preprocess.PrePostProcessor(model).input(0).tensor().setElementType(), /Wrong number of parameters./); - }); + }); - it('tensor().setElementType() throws', () => { - assert.throws(() => - new ov.preprocess.PrePostProcessor(model).input(0).tensor().setElementType('invalidType'), + it('tensor().setElementType() throws', () => { + assert.throws(() => + new ov.preprocess.PrePostProcessor(model).input(0).tensor().setElementType('invalidType'), /Cannot create ov::element::Type/); - }); + }); - it('tensor().SetShape()', () => { - assert.doesNotThrow(() => - new ov.preprocess.PrePostProcessor(model).input(0).tensor().setShape([1, 10])); - }); + it('tensor().SetShape()', () => { + assert.doesNotThrow(() => + new ov.preprocess.PrePostProcessor(model).input(0).tensor().setShape([1, 10])); + }); - it('tensor().SetShape() throws', () => { - assert.throws(() => - new ov.preprocess.PrePostProcessor(model).input(0).tensor().setShape(), + it('tensor().SetShape() throws', () => { + assert.throws(() => + new ov.preprocess.PrePostProcessor(model).input(0).tensor().setShape(), /Wrong number of parameters./); - }); + }); - it('tensor().setLayout()', () => { - assert.doesNotThrow(() => - new ov.preprocess.PrePostProcessor(model).input(0).tensor().setLayout('NHWC')); - }); + it('tensor().setLayout()', () => { + assert.doesNotThrow(() => + new ov.preprocess.PrePostProcessor(model).input(0).tensor().setLayout('NHWC')); + }); - it('tensor().setLayout() throws', () => { - assert.throws(() => - new ov.preprocess.PrePostProcessor(model).input(0).tensor().setLayout(), + it('tensor().setLayout() throws', () => { + assert.throws(() => + new ov.preprocess.PrePostProcessor(model).input(0).tensor().setLayout(), /Wrong number of parameters./); - }); + }); - it('preprocess().resize()', () => { - assert.doesNotThrow(() => - new ov.preprocess.PrePostProcessor(model).input(0).preprocess().resize( - ov.preprocess.resizeAlgorithm.RESIZE_LINEAR, - )); - }); + it('preprocess().resize()', () => { + assert.doesNotThrow(() => + new ov.preprocess.PrePostProcessor(model).input(0).preprocess().resize( + ov.preprocess.resizeAlgorithm.RESIZE_LINEAR, + )); + }); - it('preprocess().resize() throws', () => { - assert.throws(() => - new ov.preprocess.PrePostProcessor(model).input(0).preprocess().resize( - ov.preprocess.resizeAlgorithm.RESIZE_LINEAR, 'extraArg', - ), + it('preprocess().resize() throws', () => { + assert.throws(() => + new ov.preprocess.PrePostProcessor(model).input(0).preprocess().resize( + ov.preprocess.resizeAlgorithm.RESIZE_LINEAR, 'extraArg', + ), /Wrong number of parameters./); - }); + }); - it('preprocess().resize() no arg throws', () => { - assert.throws(() => - new ov.preprocess.PrePostProcessor(model).input(0).preprocess().resize(), + it('preprocess().resize() no arg throws', () => { + assert.throws(() => + new ov.preprocess.PrePostProcessor(model).input(0).preprocess().resize(), /Wrong number of parameters./); - }); + }); - it('model().setLayout()', () => { - assert.doesNotThrow(() => - new ov.preprocess.PrePostProcessor(model).input(0).model().setLayout('NCHW')); - }); + it('model().setLayout()', () => { + assert.doesNotThrow(() => + new ov.preprocess.PrePostProcessor(model).input(0).model().setLayout('NCHW')); + }); - it('model().setLayout() throws', () => { - assert.throws(() => - new ov.preprocess.PrePostProcessor(model).input(0).model().setLayout('NCHW', 'extraArg'), + it('model().setLayout() throws', () => { + assert.throws(() => + new ov.preprocess.PrePostProcessor(model).input(0).model().setLayout('NCHW', 'extraArg'), /Wrong number of parameters./); - }); + }); - it('model().setLayout() throws', () => { - assert.throws(() => - new ov.preprocess.PrePostProcessor(model).input(0).model().setLayout('invalidLayout') - ); - }); + it('model().setLayout() throws', () => { + assert.throws(() => + new ov.preprocess.PrePostProcessor(model).input(0).model().setLayout('invalidLayout') + ); + }); -}); + }); -describe('OutputInfo', () => { + describe('OutputInfo', () => { it('tensor()', () => { assert.doesNotThrow(() => new ov.preprocess.PrePostProcessor(model).output(0).tensor()); @@ -167,7 +173,7 @@ describe('OutputInfo', () => { it('tensor(param) throws', () => { assert.throws(() => new ov.preprocess.PrePostProcessor(model).output(0).tensor(0), - /Function does not take any parameters./); + /Function does not take any parameters./); }); it('tensor().setElementType()', () => { @@ -178,13 +184,13 @@ describe('OutputInfo', () => { it('tensor().setElementType() throws', () => { assert.throws(() => new ov.preprocess.PrePostProcessor(model).output(0).tensor().setElementType(), - /Wrong number of parameters./); + /Wrong number of parameters./); }); it('tensor().setElementType() throws', () => { assert.throws(() => new ov.preprocess.PrePostProcessor(model).output(0).tensor().setElementType('invalidType'), - /Cannot create ov::element::Type/); + /Cannot create ov::element::Type/); }); it('tensor().setLayout()', () => { @@ -195,7 +201,9 @@ describe('OutputInfo', () => { it('tensor().setLayout() throws', () => { assert.throws(() => new ov.preprocess.PrePostProcessor(model).output(0).tensor().setLayout(), - /Wrong number of parameters./); + /Wrong number of parameters./); }); }); + +}); diff --git a/src/bindings/js/node/tests/unit/read_model.test.js b/src/bindings/js/node/tests/unit/read_model.test.js index dbc9fc2e876b7d..47fc584fe54e9e 100644 --- a/src/bindings/js/node/tests/unit/read_model.test.js +++ b/src/bindings/js/node/tests/unit/read_model.test.js @@ -5,81 +5,90 @@ const fs = require('node:fs'); const { addon: ov } = require('../..'); const assert = require('assert'); -const { describe, it } = require('node:test'); -const { getModelPath } = require('./utils.js'); +const { describe, it, before } = require('node:test'); +const { testModels, isModelAvailable, getModelPath } = require('./utils.js'); const { xml: modelPath, bin: weightsPath } = getModelPath(); -const modelFile = fs.readFileSync(modelPath); -const modelStr = fs.readFileSync(modelPath, 'utf8'); -const weightsFile = fs.readFileSync(weightsPath); -const weightsTensor = new ov.Tensor(ov.element.u8, [weightsFile.buffer.byteLength], new Uint8Array(weightsFile.buffer)); - -const core = new ov.Core(); - -describe('Core.readModeSync', () => { - it('readModeSync(xmlPath) ', () => { - const model = core.readModelSync(modelPath); - assert.ok(model instanceof ov.Model); - assert.equal(model.inputs.length, 1); - }); - it('readModeSync(xmlPath, weightsPath) ', () => { - const model = core.readModelSync(modelPath, weightsPath); - assert.ok(model instanceof ov.Model); - assert.equal(model.inputs.length, 1); - }); +describe('Tests for reading model.', () => { - it('readModeSync throws', () => { - assert.throws( - () => core.readModelSync(core), - /'readModelSync' method called with incorrect parameters./, - ) + before(async () => { + await isModelAvailable(testModels.testModelFP32); }); - it('readModelSync(modelString, weightsTensor) ', () => { - const model = core.readModelSync( - modelStr, - weightsTensor, - ); - assert.ok(model instanceof ov.Model); - assert.equal(model.inputs.length, 1); - }); + const modelFile = fs.readFileSync(modelPath); + const modelStr = fs.readFileSync(modelPath, 'utf8'); + const weightsFile = fs.readFileSync(weightsPath); + const weightsTensor = new ov.Tensor(ov.element.u8, [weightsFile.buffer.byteLength], new Uint8Array(weightsFile.buffer)); - it('readModelSync(modelUint8ArrayBuffer, weightsUint8ArrayBuffer) ', () => { - const model = core.readModelSync( - new Uint8Array(modelFile.buffer), - new Uint8Array(weightsFile.buffer), - ); - assert.ok(model instanceof ov.Model); - assert.equal(model.inputs.length, 1); - }); -}); + const core = new ov.Core(); -describe('Core.readModel', () => { - it('readModel(xmlPath) ', async () => { - const model = await core.readModel(modelPath); - assert.equal(model.inputs.length, 1); - }); + describe('Core.readModeSync', () => { + it('readModeSync(xmlPath) ', () => { + const model = core.readModelSync(modelPath); + assert.ok(model instanceof ov.Model); + assert.equal(model.inputs.length, 1); + }); - it('readModel(xmlPath, weightsPath) ', async () => { - const model = await core.readModel(modelPath, weightsPath); - assert.equal(model.inputs.length, 1); - }); + it('readModeSync(xmlPath, weightsPath) ', () => { + const model = core.readModelSync(modelPath, weightsPath); + assert.ok(model instanceof ov.Model); + assert.equal(model.inputs.length, 1); + }); - it('readModel(modelString, weightsTensor) ', async () => { - const model = await core.readModel( - modelStr, - weightsTensor, - ); - assert.ok(model instanceof ov.Model); - assert.equal(model.inputs.length, 1); + it('readModeSync throws', () => { + assert.throws( + () => core.readModelSync(core), + /'readModelSync' method called with incorrect parameters./, + ); + }); + + it('readModelSync(modelString, weightsTensor) ', () => { + const model = core.readModelSync( + modelStr, + weightsTensor, + ); + assert.ok(model instanceof ov.Model); + assert.equal(model.inputs.length, 1); + }); + + it('readModelSync(modelUint8ArrayBuffer, weightsUint8ArrayBuffer) ', () => { + const model = core.readModelSync( + new Uint8Array(modelFile.buffer), + new Uint8Array(weightsFile.buffer), + ); + assert.ok(model instanceof ov.Model); + assert.equal(model.inputs.length, 1); + }); }); - it('readModel(modelUint8ArrayBuffer, weightsUint8ArrayBuffer) ', async () => { - const model = await core.readModel( - new Uint8Array(modelFile.buffer), - new Uint8Array(weightsFile.buffer), - ); - assert.equal(model.inputs.length, 1); + describe('Core.readModel', () => { + it('readModel(xmlPath) ', async () => { + const model = await core.readModel(modelPath); + assert.equal(model.inputs.length, 1); + }); + + it('readModel(xmlPath, weightsPath) ', async () => { + const model = await core.readModel(modelPath, weightsPath); + assert.equal(model.inputs.length, 1); + }); + + it('readModel(modelString, weightsTensor) ', async () => { + const model = await core.readModel( + modelStr, + weightsTensor, + ); + assert.ok(model instanceof ov.Model); + assert.equal(model.inputs.length, 1); + }); + + it('readModel(modelUint8ArrayBuffer, weightsUint8ArrayBuffer) ', async () => { + const model = await core.readModel( + new Uint8Array(modelFile.buffer), + new Uint8Array(weightsFile.buffer), + ); + assert.equal(model.inputs.length, 1); + }); }); + }); diff --git a/src/bindings/js/node/tests/unit/setup.js b/src/bindings/js/node/tests/unit/setup.js new file mode 100644 index 00000000000000..b4885d24157abe --- /dev/null +++ b/src/bindings/js/node/tests/unit/setup.js @@ -0,0 +1,9 @@ +const { testModels, downloadTestModel } = require('./utils.js'); + +if (require.main === module) { + main(); +} + +async function main() { + await downloadTestModel(testModels.testModelFP32); +} diff --git a/src/bindings/js/node/tests/unit/tensor.test.js b/src/bindings/js/node/tests/unit/tensor.test.js index 79e0a66a9cdfe7..45d974f54cd6c0 100644 --- a/src/bindings/js/node/tests/unit/tensor.test.js +++ b/src/bindings/js/node/tests/unit/tensor.test.js @@ -7,243 +7,246 @@ const assert = require('assert'); const { test, describe, it } = require('node:test'); const getRandomBigInt = require('random-bigint'); -const shape = [1, 3, 224, 224]; -const elemNum = 1 * 3 * 224 * 224; -const data = Float32Array.from({ length: elemNum }, () => Math.random() ); -const params = [ - [ov.element.i8, 'i8', Int8Array.from({ length: elemNum }, () => Math.random() )], - [ov.element.u8, 'u8', Uint8Array.from({ length: elemNum }, () => Math.random() )], - [ov.element.i16, 'i16', Int16Array.from({ length: elemNum }, () => Math.random() )], - [ov.element.u16, 'u16', Uint16Array.from({ length: elemNum }, () => Math.random() )], - [ov.element.i32, 'i32', Int32Array.from({ length: elemNum }, () => Math.random() )], - [ov.element.u32, 'u32', Uint32Array.from({ length: elemNum }, () => Math.random() )], - [ov.element.f32, 'f32', Float32Array.from({ length: elemNum }, () => Math.random() )], - [ov.element.f64, 'f64', Float64Array.from({ length: elemNum }, () => Math.random() )], - [ov.element.i64, 'i64', BigInt64Array.from({ length: elemNum }, () => getRandomBigInt(10) )], - [ov.element.u64, 'u64', BigUint64Array.from({ length: elemNum }, () => getRandomBigInt(10) )], -]; - -test('Test for number of arguments in tensor', () => { - assert.throws( () => new ov.Tensor(ov.element.f32, shape, data, params), - {message: /Invalid number of arguments for Tensor constructor./}); -}); - -describe('Tensor without data parameters', () => { - it('Tensor should have array with zeros and numbers of elements according to the shape', () => { - const tensor = new ov.Tensor(ov.element.f32, shape); - assert.strictEqual(tensor.data.length, elemNum); +describe('ov.Tensor tests', () => { + + const shape = [1, 3, 224, 224]; + const elemNum = 1 * 3 * 224 * 224; + const data = Float32Array.from({ length: elemNum }, () => Math.random() ); + const params = [ + [ov.element.i8, 'i8', Int8Array.from({ length: elemNum }, () => Math.random() )], + [ov.element.u8, 'u8', Uint8Array.from({ length: elemNum }, () => Math.random() )], + [ov.element.i16, 'i16', Int16Array.from({ length: elemNum }, () => Math.random() )], + [ov.element.u16, 'u16', Uint16Array.from({ length: elemNum }, () => Math.random() )], + [ov.element.i32, 'i32', Int32Array.from({ length: elemNum }, () => Math.random() )], + [ov.element.u32, 'u32', Uint32Array.from({ length: elemNum }, () => Math.random() )], + [ov.element.f32, 'f32', Float32Array.from({ length: elemNum }, () => Math.random() )], + [ov.element.f64, 'f64', Float64Array.from({ length: elemNum }, () => Math.random() )], + [ov.element.i64, 'i64', BigInt64Array.from({ length: elemNum }, () => getRandomBigInt(10) )], + [ov.element.u64, 'u64', BigUint64Array.from({ length: elemNum }, () => getRandomBigInt(10) )], + ]; + + test('Test for number of arguments in tensor', () => { + assert.throws( () => new ov.Tensor(ov.element.f32, shape, data, params), + {message: /Invalid number of arguments for Tensor constructor./}); + }); + + describe('Tensor without data parameters', () => { + it('Tensor should have array with zeros and numbers of elements according to the shape', () => { + const tensor = new ov.Tensor(ov.element.f32, shape); + assert.strictEqual(tensor.data.length, elemNum); + }); }); -}); -describe('Tensor data', () => { + describe('Tensor data', () => { - params.forEach(([type, stringType, data]) => { - it(`Set tensor data with ${stringType} element type`, () => { - const tensor = new ov.Tensor(type, shape, data); - assert.deepStrictEqual(tensor.data, data); + params.forEach(([type, stringType, data]) => { + it(`set tensor data with ${stringType} element type`, () => { + const tensor = new ov.Tensor(type, shape, data); + assert.deepStrictEqual(tensor.data, data); + }); }); - }); - it('Create string tensor', () => { - const str_arr = ['text', 'more text', 'even more text']; - const tensor = new ov.Tensor(str_arr); - assert.deepStrictEqual(tensor.data, str_arr); - }); + it('create string tensor', () => { + const str_arr = ['text', 'more text', 'even more text']; + const tensor = new ov.Tensor(str_arr); + assert.deepStrictEqual(tensor.data, str_arr); + }); - it('Create string tensor', () => { - const str_arr = ['text', 'more text', 'even more text']; - const tensor = new ov.Tensor(str_arr); - assert.deepStrictEqual(tensor.data, str_arr); - }); + it('create string tensor', () => { + const str_arr = ['text', 'more text', 'even more text']; + const tensor = new ov.Tensor(str_arr); + assert.deepStrictEqual(tensor.data, str_arr); + }); - it('String tensor - passed array does not contain string elements', () => { - const str_arr = ['text', true]; - assert.throws(() => { new ov.Tensor(str_arr);}, - /The array passed to create string tensor must contain only strings./ - ); - }); + it('string tensor - passed array does not contain string elements', () => { + const str_arr = ['text', true]; + assert.throws(() => { new ov.Tensor(str_arr);}, + /The array passed to create string tensor must contain only strings./ + ); + }); - it('Set string tensor data', () => { - const str_arr = ['H', 'e', 'l', 'l', 'o']; - const tensor = new ov.Tensor(ov.element.string, [1, 1, 1, 5]); - tensor.data = str_arr; - assert.deepStrictEqual(tensor.data, str_arr); - }); + it('set string tensor data', () => { + const str_arr = ['H', 'e', 'l', 'l', 'o']; + const tensor = new ov.Tensor(ov.element.string, [1, 1, 1, 5]); + tensor.data = str_arr; + assert.deepStrictEqual(tensor.data, str_arr); + }); - it('Test tensor getData()', () => { - const tensor = new ov.Tensor(ov.element.f32, shape, data); - assert.deepStrictEqual(tensor.getData(), data); - }); + it('test tensor getData()', () => { + const tensor = new ov.Tensor(ov.element.f32, shape, data); + assert.deepStrictEqual(tensor.getData(), data); + }); - it('Test tensor.data setter - different element type throws', () => { - const float64_data = Float64Array.from([1, 2, 3] ); - const tensor = new ov.Tensor(ov.element.f32, [1, 3]); - assert.throws(() => { - tensor.data = float64_data;}, - /Passed array must have the same size as the Tensor!/ - ); - }); + it('test tensor.data setter - different element type throws', () => { + const float64_data = Float64Array.from([1, 2, 3] ); + const tensor = new ov.Tensor(ov.element.f32, [1, 3]); + assert.throws(() => { + tensor.data = float64_data;}, + /Passed array must have the same size as the Tensor!/ + ); + }); - it('Test tensor.data setter - different element length throws', () => { - const float64_data = Float64Array.from([1, 2, 3] ); - const tensor = new ov.Tensor(ov.element.f64, [1, 2]); - assert.throws(() => { - tensor.data = float64_data;}, - /Passed array must have the same size as the Tensor!/ - ); - }); + it('test tensor.data setter - different element length throws', () => { + const float64_data = Float64Array.from([1, 2, 3] ); + const tensor = new ov.Tensor(ov.element.f64, [1, 2]); + assert.throws(() => { + tensor.data = float64_data;}, + /Passed array must have the same size as the Tensor!/ + ); + }); - it('Test tensor.data setter', () => { - const testString = 'test'; - const tensor = new ov.Tensor(ov.element.f64, [1, 2]); - assert.throws(() => { - tensor.data = testString;}, - /Passed argument must be TypedArray, or Array if the tensor type is string./ - ); - }); + it('test tensor.data setter', () => { + const testString = 'test'; + const tensor = new ov.Tensor(ov.element.f64, [1, 2]); + assert.throws(() => { + tensor.data = testString;}, + /Passed argument must be TypedArray, or Array if the tensor type is string./ + ); + }); - it('Test tensor.data setter', () => { - const tensor = new ov.Tensor(ov.element.f32, shape); - tensor.data = data; - assert.deepStrictEqual(tensor.getData(), data); - }); + it('test tensor.data setter', () => { + const tensor = new ov.Tensor(ov.element.f32, shape); + tensor.data = data; + assert.deepStrictEqual(tensor.getData(), data); + }); - it('Set tensor data with Float32Array created from ArrayBuffer', () => { - const size = elemNum * 4; - const buffer = new ArrayBuffer(size); - const view = new Float32Array(buffer); - view.set(data); - const tensor = new ov.Tensor(ov.element.f32, shape, view); - assert.deepStrictEqual(tensor.data, data); - }); + it('set tensor data with Float32Array created from ArrayBuffer', () => { + const size = elemNum * 4; + const buffer = new ArrayBuffer(size); + const view = new Float32Array(buffer); + view.set(data); + const tensor = new ov.Tensor(ov.element.f32, shape, view); + assert.deepStrictEqual(tensor.data, data); + }); - it('Set tensor data with too big Float32Array', () => { - const size = elemNum * 8; - const buffer = new ArrayBuffer(size); - const view = new Float32Array(buffer); - view.set(data); - assert.throws( () => new ov.Tensor(ov.element.f32, shape, view), - {message: /Memory allocated using shape and element::type mismatch/}); - }); + it('set tensor data with too big Float32Array', () => { + const size = elemNum * 8; + const buffer = new ArrayBuffer(size); + const view = new Float32Array(buffer); + view.set(data); + assert.throws( () => new ov.Tensor(ov.element.f32, shape, view), + {message: /Memory allocated using shape and element::type mismatch/}); + }); - it('Third argument of a tensor cannot be an ArrayBuffer', () => { - assert.throws( - () => new ov.Tensor(ov.element.f32, shape, new ArrayBuffer(1234)), - {message: /Third argument of a tensor must be TypedArray./}); - }); + it('third argument of a tensor cannot be an ArrayBuffer', () => { + assert.throws( + () => new ov.Tensor(ov.element.f32, shape, new ArrayBuffer(1234)), + {message: /Third argument of a tensor must be TypedArray./}); + }); - it('Third argument of a tensor cannot be an array object', () => { - assert.throws( - () => new ov.Tensor(ov.element.f32, shape, [1, 2, 3, 4]), - {message: /Third argument of a tensor must be TypedArray./}); + it('third argument of a tensor cannot be an array object', () => { + assert.throws( + () => new ov.Tensor(ov.element.f32, shape, [1, 2, 3, 4]), + {message: /Third argument of a tensor must be TypedArray./}); + }); }); -}); -describe('Tensor shape', () => { + describe('Tensor shape', () => { - it('ov::Shape from an array object', () => { - const tensor = new ov.Tensor(ov.element.f32, [1, 3, 224, 224], data); - assert.deepStrictEqual(tensor.getShape(), [1, 3, 224, 224]); - }); + it('ov::Shape from an array object', () => { + const tensor = new ov.Tensor(ov.element.f32, [1, 3, 224, 224], data); + assert.deepStrictEqual(tensor.getShape(), [1, 3, 224, 224]); + }); - it('ov::Shape from an array object with floating point numbers', () => { - const tensor = + it('ov::Shape from an array object with floating point numbers', () => { + const tensor = new ov.Tensor(ov.element.f32, [1, 3.0, 224.8, 224.4], data); - assert.deepStrictEqual(tensor.getShape(), [1, 3, 224, 224]); - }); + assert.deepStrictEqual(tensor.getShape(), [1, 3, 224, 224]); + }); - it('Array argument to create ov::Shape can only contain numbers', () => { - assert.throws( - () => new ov.Tensor(ov.element.f32, ['1', 3, 224, 224], data), - {message: /Passed array must contain only numbers/}); - }); + it('array argument to create ov::Shape can only contain numbers', () => { + assert.throws( + () => new ov.Tensor(ov.element.f32, ['1', 3, 224, 224], data), + {message: /Passed array must contain only numbers/}); + }); - it('ov::Shape from TypedArray -> Int32Array', () => { - const shp = Int32Array.from([1, 224, 224, 3]); - const tensor = new ov.Tensor(ov.element.f32, shp, data); - assert.deepStrictEqual(tensor.getShape(), [1, 224, 224, 3]); - }); + it('ov::Shape from TypedArray -> Int32Array', () => { + const shp = Int32Array.from([1, 224, 224, 3]); + const tensor = new ov.Tensor(ov.element.f32, shp, data); + assert.deepStrictEqual(tensor.getShape(), [1, 224, 224, 3]); + }); - it('Cannot create ov::Shape from Float32Array', () => { - const shape = Float32Array.from([1, 224, 224, 3]); - assert.throws( - () => new ov.Tensor(ov.element.f32, shape, data), - /Passed argument must be an Int32Array or a Uint32Array./ - ); - }); + it('cannot create ov::Shape from Float32Array', () => { + const shape = Float32Array.from([1, 224, 224, 3]); + assert.throws( + () => new ov.Tensor(ov.element.f32, shape, data), + /Passed argument must be an Int32Array or a Uint32Array./ + ); + }); - it('Cannot create ov::Shape from ArrayBuffer', () => { - const shape = Int32Array.from([1, 224, 224, 3]); - assert.throws( - () => new ov.Tensor(ov.element.f32, shape.buffer, data), - /Passed argument must be of type Array or TypedArray./ - ); - }); + it('cannot create ov::Shape from ArrayBuffer', () => { + const shape = Int32Array.from([1, 224, 224, 3]); + assert.throws( + () => new ov.Tensor(ov.element.f32, shape.buffer, data), + /Passed argument must be of type Array or TypedArray./ + ); + }); - it('getShape() method does not accept parameters', () => { - const tensor = new ov.Tensor(ov.element.f32, [1, 3, 224, 224], data); - assert.throws( - () => tensor.getShape(1, 2, 3), - { message: 'No parameters are allowed for the getShape() method.'} - ); + it('getShape() method does not accept parameters', () => { + const tensor = new ov.Tensor(ov.element.f32, [1, 3, 224, 224], data); + assert.throws( + () => tensor.getShape(1, 2, 3), + { message: 'No parameters are allowed for the getShape() method.'} + ); + }); }); -}); -describe('Tensor element type', () => { - params.forEach(([elemType, val]) => { - it(`Comparison of ov.element.${elemType} to string ${val}`, () => { - assert.strictEqual(elemType, val); + describe('Tensor element type', () => { + params.forEach(([elemType, val]) => { + it(`comparison of ov.element.${elemType} to string ${val}`, () => { + assert.strictEqual(elemType, val); + }); }); - }); - params.forEach(([elemType, , data]) => { - it(`Comparison of ov.element ${elemType} got from Tensor object`, () => { - const tensor = new ov.Tensor(elemType, shape, data); - assert.strictEqual(tensor.getElementType(), elemType); + params.forEach(([elemType, , data]) => { + it(`comparison of ov.element ${elemType} got from Tensor object`, () => { + const tensor = new ov.Tensor(elemType, shape, data); + assert.strictEqual(tensor.getElementType(), elemType); + }); }); }); -}); -describe('Tensor getSize', () => { + describe('Tensor getSize', () => { - it('getSize returns the correct total number of elements', () => { - const tensor = new ov.Tensor(ov.element.f32, shape, data); - const expectedSize = shape.reduce((acc, dim) => acc * dim, 1); - assert.strictEqual(tensor.getSize(), expectedSize); - }); + it('getSize returns the correct total number of elements', () => { + const tensor = new ov.Tensor(ov.element.f32, shape, data); + const expectedSize = shape.reduce((acc, dim) => acc * dim, 1); + assert.strictEqual(tensor.getSize(), expectedSize); + }); - it('getSize should throw an error if arguments are provided', () => { - const tensor = new ov.Tensor(ov.element.f32, shape, data); - assert.throws( - () => tensor.getSize(1), - { message: 'getSize() does not accept any arguments.' } - ); + it('getSize should throw an error if arguments are provided', () => { + const tensor = new ov.Tensor(ov.element.f32, shape, data); + assert.throws( + () => tensor.getSize(1), + { message: 'getSize() does not accept any arguments.' } + ); + }); }); -}); -describe('Tensor getSize for various shapes', () => { + describe('Tensor getSize for various shapes', () => { - it('calculates size correctly for a common image data shape [3, 224, 224]', () => { - const shape = [3, 224, 224]; - const expectedSize = 3*224*224; - const tensorData = new Float32Array(expectedSize).fill(0); - const tensor = new ov.Tensor(ov.element.f32, shape, tensorData); - assert.strictEqual(tensor.getSize(), expectedSize); - }); + it('calculates size correctly for a common image data shape [3, 224, 224]', () => { + const shape = [3, 224, 224]; + const expectedSize = 3*224*224; + const tensorData = new Float32Array(expectedSize).fill(0); + const tensor = new ov.Tensor(ov.element.f32, shape, tensorData); + assert.strictEqual(tensor.getSize(), expectedSize); + }); - it('calculates size correctly for a scalar wrapped in a tensor [1]', () => { - const shape = [1]; - const expectedSize = 1; - const tensorData = new Float32Array(expectedSize).fill(0); - const tensor = new ov.Tensor(ov.element.f32, shape, tensorData); - assert.strictEqual(tensor.getSize(), expectedSize); - }); + it('calculates size correctly for a scalar wrapped in a tensor [1]', () => { + const shape = [1]; + const expectedSize = 1; + const tensorData = new Float32Array(expectedSize).fill(0); + const tensor = new ov.Tensor(ov.element.f32, shape, tensorData); + assert.strictEqual(tensor.getSize(), expectedSize); + }); - it('calculates size correctly for a vector [10]', () => { - const shape = [10]; - const expectedSize = 10; - const tensorData = new Float32Array(expectedSize).fill(0); - const tensor = new ov.Tensor(ov.element.f32, shape, tensorData); - assert.strictEqual(tensor.getSize(), expectedSize); + it('calculates size correctly for a vector [10]', () => { + const shape = [10]; + const expectedSize = 10; + const tensorData = new Float32Array(expectedSize).fill(0); + const tensor = new ov.Tensor(ov.element.f32, shape, tensorData); + assert.strictEqual(tensor.getSize(), expectedSize); + }); }); }); diff --git a/src/bindings/js/node/tests/unit/test_models/test_model_fp32.bin b/src/bindings/js/node/tests/unit/test_models/test_model_fp32.bin deleted file mode 100644 index b5f85e4108314c14a05efa9e4e2e149cf446b87d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 337592 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

pg0IZ22vGgPF(b_tKG=Z75mq2ZqCwNJdv2$nhj# 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 diff --git a/src/bindings/js/node/tests/unit/test_models/test_model_fp32.xml b/src/bindings/js/node/tests/unit/test_models/test_model_fp32.xml deleted file mode 100644 index dcf0cd74b068ac..00000000000000 --- a/src/bindings/js/node/tests/unit/test_models/test_model_fp32.xml +++ /dev/null @@ -1,467 +0,0 @@ - - - - - - - - 1 - 3 - 32 - 32 - - - - - - - - 16 - 3 - 5 - 5 - - - - - - - - 1 - 3 - 32 - 32 - - - 16 - 3 - 5 - 5 - - - - - 1 - 16 - 32 - 32 - - - - - - - - 1 - 16 - 1 - 1 - - - - - - - 1 - 16 - 32 - 32 - - - 1 - 16 - 1 - 1 - - - - - 1 - 16 - 32 - 32 - - - - - - - 1 - 16 - 32 - 32 - - - - - 1 - 16 - 32 - 32 - - - - - - - - 1 - 16 - 32 - 32 - - - - - 1 - 16 - 16 - 16 - - - - - - - - 32 - 16 - 5 - 5 - - - - - - - - 1 - 16 - 16 - 16 - - - 32 - 16 - 5 - 5 - - - - - 1 - 32 - 16 - 16 - - - - - - - - 1 - 32 - 1 - 1 - - - - - - - 1 - 32 - 16 - 16 - - - 1 - 32 - 1 - 1 - - - - - 1 - 32 - 16 - 16 - - - - - - - - 64 - 32 - 3 - 3 - - - - - - - - 1 - 32 - 16 - 16 - - - 64 - 32 - 3 - 3 - - - - - 1 - 64 - 18 - 18 - - - - - - - - 1 - 64 - 1 - 1 - - - - - - - 1 - 64 - 18 - 18 - - - 1 - 64 - 1 - 1 - - - - - 1 - 64 - 18 - 18 - - - - - - - 1 - 64 - 18 - 18 - - - - - 1 - 64 - 18 - 18 - - - - - - - - 1 - 64 - 18 - 18 - - - - - 1 - 64 - 9 - 9 - - - - - - - - 2 - - - - - - - - 1 - 64 - 9 - 9 - - - 2 - - - - - 1 - 5184 - - - - - - - - 10 - 5184 - - - - - - - - 1 - 5184 - - - 10 - 5184 - - - - - 1 - 10 - - - - - - - - 1 - 10 - - - - - - - 1 - 10 - - - 1 - 10 - - - - - 1 - 10 - - - - - - - - 1 - 10 - - - - - 1 - 10 - - - - - - - 1 - 10 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/bindings/js/node/tests/unit/utils.js b/src/bindings/js/node/tests/unit/utils.js index ebe126d6c74129..cf541a332b103f 100644 --- a/src/bindings/js/node/tests/unit/utils.js +++ b/src/bindings/js/node/tests/unit/utils.js @@ -3,15 +3,67 @@ // SPDX-License-Identifier: Apache-2.0 const path = require('path'); +const fs = require('node:fs/promises'); +const { downloadFile, checkIfPathExists } = require('../../scripts/download_runtime'); -module.exports = { getModelPath }; +const modelDir = 'tests/unit/test_models/'; +const testModels = { + testModelFP32: { + xml: 'test_model_fp32.xml', + bin: 'test_model_fp32.bin', + xmlURL: + 'https://raw.githubusercontent.com/openvinotoolkit/testdata/master/models/test_model/test_model_fp32.xml', + binURL: + 'https://media.githubusercontent.com/media/openvinotoolkit/testdata/master/models/test_model/test_model_fp32.bin', + }, +}; + +module.exports = { + getModelPath, + downloadTestModel, + isModelAvailable, + testModels, +}; function getModelPath(isFP16=false) { - const basePath = 'tests/unit/test_models/'; const modelName = `test_model_fp${isFP16 ? 16 : 32}`; return { - xml: path.join(basePath, `${modelName}.xml`), - bin: path.join(basePath, `${modelName}.bin`), + xml: path.join(modelDir, `${modelName}.xml`), + bin: path.join(modelDir, `${modelName}.bin`), }; } + +async function downloadTestModel(model) { + const modelsDir = './tests/unit/test_models'; + try { + const ifModelsDirectoryExists = await checkIfPathExists(modelsDir); + if (!ifModelsDirectoryExists) { + await fs.mkdir(modelDir); + } + + const modelPath = path.join(modelsDir, model.xml); + const modelExists = await checkIfPathExists(modelPath); + if (modelExists) return; + + const { env } = process; + const proxyUrl = env.http_proxy || env.HTTP_PROXY || env.npm_config_proxy; + + await downloadFile(model.xmlURL, modelsDir, model.xml, proxyUrl); + await downloadFile(model.binURL, modelsDir, model.bin, proxyUrl); + } catch(error) { + console.error(`Failed to download the model: ${error}.`); + throw error; + } +} + +async function isModelAvailable(model) { + const baseArtifactsDir = './tests/unit/test_models'; + const modelPath = path.join(baseArtifactsDir, model.xml); + const modelExists = await checkIfPathExists(modelPath); + if (modelExists) return; + + console.log('\n\nTestModel cannot be found.\nPlease run `npm run test_setup`.\n\n'); + process.exit(1); + +} From 3afad8df203a9354b314e32c79d088e358a043b8 Mon Sep 17 00:00:00 2001 From: Aleksandr Voron Date: Wed, 21 Aug 2024 15:52:32 +0200 Subject: [PATCH 03/35] [CPU][ARM] Upgrade ACL to 24.08 (#26137) - [x] Performance check ### Tickets: - [CVS-150178](https://jira.devtools.intel.com/browse/CVS-150178) --- src/plugins/intel_cpu/thirdparty/ComputeLibrary | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/intel_cpu/thirdparty/ComputeLibrary b/src/plugins/intel_cpu/thirdparty/ComputeLibrary index c5dd7753d0475f..f1929dc994d8e5 160000 --- a/src/plugins/intel_cpu/thirdparty/ComputeLibrary +++ b/src/plugins/intel_cpu/thirdparty/ComputeLibrary @@ -1 +1 @@ -Subproject commit c5dd7753d0475ffec0f192f3181fe67a1d761680 +Subproject commit f1929dc994d8e5afae5c77ca66446344119a8592 From 6f899d293a10bc100c8fbc39fc0009d31601a51d Mon Sep 17 00:00:00 2001 From: Haiqi Pan Date: Wed, 21 Aug 2024 22:37:31 +0800 Subject: [PATCH 04/35] Move INFERENCE_PRECISION_HINT test to optional conformance for meta-plugin (#25598) ### Details: - *Move INFERENCE_PRECISION_HINT test to optional conformance for meta-plugin* ### Tickets: - *147702* --------- Co-authored-by: Chen Peter --- .../src/ov_plugin/properties.cpp | 11 ++++++----- .../shared/include/base/ov_behavior_test_utils.hpp | 6 ++++++ .../github/skip_configs/CPU/expected_failures_API.csv | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/tests/functional/plugin/conformance/test_runner/api_conformance_runner/src/ov_plugin/properties.cpp b/src/tests/functional/plugin/conformance/test_runner/api_conformance_runner/src/ov_plugin/properties.cpp index 5ed1e4269d69c4..594f4c68b61dc9 100644 --- a/src/tests/functional/plugin/conformance/test_runner/api_conformance_runner/src/ov_plugin/properties.cpp +++ b/src/tests/functional/plugin/conformance/test_runner/api_conformance_runner/src/ov_plugin/properties.cpp @@ -88,11 +88,12 @@ INSTANTIATE_TEST_SUITE_P(ov_plugin_mandatory, OVCheckChangePropComplieModleGetPr ::testing::Values(ov::AnyMap({}))), OVCheckChangePropComplieModleGetPropTests_DEVICE_ID::getTestCaseName); -INSTANTIATE_TEST_SUITE_P(ov_plugin_mandatory, OVCheckChangePropComplieModleGetPropTests_InferencePrecision, - ::testing::Combine( - ::testing::Values(ov::test::utils::target_device), - ::testing::Values(ov::AnyMap({}))), - OVCheckChangePropComplieModleGetPropTests_InferencePrecision::getTestCaseName); +/* Add prefix mandatory_ to suffix (getTestCaseName) of HW plugin test cases */ +INSTANTIATE_TEST_SUITE_P( + ov_plugin, + OVCheckChangePropComplieModleGetPropTests_InferencePrecision, + ::testing::Combine(::testing::Values(ov::test::utils::target_device), ::testing::Values(ov::AnyMap({}))), + MARK_MANDATORY_FOR_HW_DEVICE(OVCheckChangePropComplieModleGetPropTests_InferencePrecision::getTestCaseName)); INSTANTIATE_TEST_SUITE_P(ov_plugin, OVCheckMetricsPropsTests_ModelDependceProps, ::testing::Combine( diff --git a/src/tests/functional/plugin/shared/include/base/ov_behavior_test_utils.hpp b/src/tests/functional/plugin/shared/include/base/ov_behavior_test_utils.hpp index 9de110a82b3032..686a3c797ba917 100644 --- a/src/tests/functional/plugin/shared/include/base/ov_behavior_test_utils.hpp +++ b/src/tests/functional/plugin/shared/include/base/ov_behavior_test_utils.hpp @@ -30,6 +30,12 @@ #include "common_test_utils/subgraph_builders/concat_with_params.hpp" #include "common_test_utils/subgraph_builders/split_concat.hpp" +#define MARK_MANDATORY_FOR_HW_DEVICE(GET_TEST_NAME) \ + [&](const testing::TestParamInfo& info) { \ + std::string name = GET_TEST_NAME(info); \ + return sw_plugin_in_target_device(ov::test::utils::target_device) ? "" : "mandatory_" + name; \ + } + namespace ov { namespace test { namespace behavior { diff --git a/src/tests/test_utils/functional_test_utils/layer_tests_summary/github/skip_configs/CPU/expected_failures_API.csv b/src/tests/test_utils/functional_test_utils/layer_tests_summary/github/skip_configs/CPU/expected_failures_API.csv index 52b257b6251142..ba96df69cf0887 100644 --- a/src/tests/test_utils/functional_test_utils/layer_tests_summary/github/skip_configs/CPU/expected_failures_API.csv +++ b/src/tests/test_utils/functional_test_utils/layer_tests_summary/github/skip_configs/CPU/expected_failures_API.csv @@ -1,5 +1,5 @@ Test Name,Fix Priority -ov_plugin_mandatory/OVCheckChangePropComplieModleGetPropTests_InferencePrecision.ChangeCorrectProperties/target_device=CPU_,1.0 +ov_plugin/OVCheckChangePropComplieModleGetPropTests_InferencePrecision.ChangeCorrectProperties/mandatory_target_device=CPU_,1.0 ov_compiled_model_mandatory/OVClassCompiledModelGetPropertyTest_MODEL_PRIORITY.GetMetricNoThrow/3,1.0 ov_compiled_model_mandatory/OVClassCompiledModelGetPropertyTest_MODEL_PRIORITY.GetMetricNoThrow/2,1.0 ov_compiled_model_mandatory/OVClassCompiledModelGetPropertyTest_MODEL_PRIORITY.GetMetricNoThrow/1,1.0 From 407f0bcf25454f90a0c7a05ecaafe224e6ffc71c Mon Sep 17 00:00:00 2001 From: Andrei Kashchikhin Date: Wed, 21 Aug 2024 16:29:27 +0100 Subject: [PATCH 05/35] [CI] [GHA] Do not checkout latest oneDNN on U22 in nightly (#26150) ### Details: - GPU tests are only enabled for U20. Checking out the latest oneDNN via https://github.com/openvinotoolkit/openvino/blob/0b8eb87e0efa9030be47d661fe94b3c8beb655fd/.github/workflows/job_build_linux.yml#L71 breaks the build on U22: [like this one](https://github.com/openvinotoolkit/openvino/actions/runs/10481070002/job/29030046664) and prevents other nightly tests from executing. - This PR limits the checkout of the latest oneDNN to U20 only. This will unblock the nightly tests that run on U22 for now. - Later, it might be better to separate the builds for GPU/other test jobs. ### Tickets: - *149805* --- .github/workflows/job_build_linux.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/job_build_linux.yml b/.github/workflows/job_build_linux.yml index 8fd35f48172a85..564cd8e58c38a7 100644 --- a/.github/workflows/job_build_linux.yml +++ b/.github/workflows/job_build_linux.yml @@ -69,7 +69,7 @@ jobs: # Ticket: 139627 - name: Checkout the latest OneDNN for GPU in nightly - if: ${{ inputs.event-name == 'schedule' }} + if: ${{ inputs.event-name == 'schedule' && inputs.os == 'ubuntu_20_04' }} # GPU tests are enabled only on U20 working-directory: ${{ env.OPENVINO_REPO }}/src/plugins/intel_gpu/thirdparty/onednn_gpu run: | git fetch origin From 9d09d0a8fbf356cf3b6b11d4ca39485d82f45e79 Mon Sep 17 00:00:00 2001 From: Andrew Kwangwoong Park Date: Thu, 22 Aug 2024 01:39:30 +0900 Subject: [PATCH 06/35] [GPU] Fix segfault in layer tests for onnx_tests.test_lstm.TestLSTM (#26159) ### Details: - This issue occurs after https://github.com/openvinotoolkit/openvino/pull/26032 - Fix segfault in layer tests for onnx_tests.test_lstm.TestLSTM ### Tickets: - [CVS-150221](https://jira.devtools.intel.com/browse/CVS-150221) --- src/plugins/intel_gpu/src/graph/include/reshape_inst.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/intel_gpu/src/graph/include/reshape_inst.h b/src/plugins/intel_gpu/src/graph/include/reshape_inst.h index d5a60138a50777..a94f48a6ec92dd 100644 --- a/src/plugins/intel_gpu/src/graph/include/reshape_inst.h +++ b/src/plugins/intel_gpu/src/graph/include/reshape_inst.h @@ -44,7 +44,7 @@ struct typed_program_node : public typed_program_node_base { return false; // oneDNN supports padded input of outer axis only for buffer fusing on static shape - if (!has_outer_padding_offset() && get_users().front()->get_preferred_impl_type() == impl_types::onednn) + if (!has_outer_padding_offset() && get_users().size() == 1 && get_users().front()->get_preferred_impl_type() == impl_types::onednn) return false; // TODO: If user is RoPE or MVN and dynamic padding exists, ouput padding propagation is not supported in the base mode From 9ef7e23da022f20e128d6b2265aeb4cdf22f382c Mon Sep 17 00:00:00 2001 From: ge0rdi Date: Wed, 21 Aug 2024 18:42:33 +0200 Subject: [PATCH 07/35] [NPU] Fix ze_loader dependency (#26157) ### Details: Commit ad4eb09 introduced dynamic loading of ze_loader so that it is loaded only when really needed. Recent changes in `ZeroRemoteTensor` caused NPU plugin binary to import from ze_loader directly. This commit should fix that and the NPU binary should have no longer dependency on ze_loader. ### Tickets: - None --- src/plugins/intel_npu/src/backend/src/zero_remote_tensor.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/intel_npu/src/backend/src/zero_remote_tensor.cpp b/src/plugins/intel_npu/src/backend/src/zero_remote_tensor.cpp index d27ee74d9e5d1e..4ac1d75fe57f10 100644 --- a/src/plugins/intel_npu/src/backend/src/zero_remote_tensor.cpp +++ b/src/plugins/intel_npu/src/backend/src/zero_remote_tensor.cpp @@ -7,6 +7,7 @@ #include #include "intel_npu/al/config/common.hpp" +#include "intel_npu/utils/zero/zero_api.hpp" #include "openvino/core/type/element_iterator.hpp" #include "zero_utils.hpp" From 50ffcbc7e96a46ff7799a6330ad63230bdd525e7 Mon Sep 17 00:00:00 2001 From: Ilya Lavrenov Date: Wed, 21 Aug 2024 23:45:05 +0400 Subject: [PATCH 08/35] Fixed pattern for patching TBB config files (#26167) ### Details: - To fix issues with tokenizers build - See https://github.com/openvinotoolkit/openvino_tokenizers/pull/229 --- src/bindings/python/wheel/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bindings/python/wheel/setup.py b/src/bindings/python/wheel/setup.py index d91523659a0a78..f88d9bd3da7592 100644 --- a/src/bindings/python/wheel/setup.py +++ b/src/bindings/python/wheel/setup.py @@ -487,7 +487,7 @@ def copy_package_data(self, src_dirs): tbb_replacements = { # change the path where the TBBConfig.cmake is installed (/lib/cmake/TBB -> openvino/cmake) - r"(\{CMAKE_CURRENT_LIST_FILE\})": r"\1/fake_dir", + r"(_IMPORT_PREFIX \"\$\{CMAKE_CURRENT_LIST_FILE\})": r"\1/fake_dir", # change the path where the libraries are installed (/lib -> openvino/libs) r"(\{_IMPORT_PREFIX\})\/(.*)\/(.+\.[lib|dylib|so|dll])": rf"\1/{WHEEL_LIBS_INSTALL_DIR}/\3", # change the path where the include files are installed (/include -> openvino/include) From 28950f660e0344984ab553ada973eec4465d20ff Mon Sep 17 00:00:00 2001 From: Roman Kazantsev Date: Thu, 22 Aug 2024 00:26:56 +0400 Subject: [PATCH 09/35] [Common FE] Document get_input_by_reference better (#26165) **Details:** Document get_input_by_reference better **Ticket:** TBD Signed-off-by: Kazantsev, Roman --- .../common/include/openvino/frontend/node_context.hpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/frontends/common/include/openvino/frontend/node_context.hpp b/src/frontends/common/include/openvino/frontend/node_context.hpp index 98243fab0e9c63..c184d59eb32ab0 100644 --- a/src/frontends/common/include/openvino/frontend/node_context.hpp +++ b/src/frontends/common/include/openvino/frontend/node_context.hpp @@ -48,7 +48,12 @@ class FRONTEND_API NodeContext { FRONT_END_NOT_IMPLEMENTED(get_input); } - /// \brief Returns the input by reference. The reference value can be changed by consuming operation + /// \brief Returns output of Variable node (or Variable value). + /// Variable is a special node that stores a value represented with a sub-graph. + /// Variable has a concrete value at each conversion step. + /// The current (consuming) operation node can change its value + /// so consumers of this Variable will have a new value at next conversion steps. + /// See ov::frontend::Variable class for more details. virtual Output get_input_by_reference(int idx) const { FRONT_END_NOT_IMPLEMENTED(get_input_by_reference); } From 711f06026f7ba35f4c2482f0e3d398d1be51ab12 Mon Sep 17 00:00:00 2001 From: Roman Kazantsev Date: Thu, 22 Aug 2024 01:39:35 +0400 Subject: [PATCH 10/35] Revert "Temporarily remove TF layer tests from required on ARM" (#26169) Reverts openvinotoolkit/openvino#26158 Issue with TBB on ARM was fixed. --- .github/workflows/linux_arm64.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linux_arm64.yml b/.github/workflows/linux_arm64.yml index 225fd632ee81c3..d38eda93e7d2b8 100644 --- a/.github/workflows/linux_arm64.yml +++ b/.github/workflows/linux_arm64.yml @@ -434,7 +434,7 @@ jobs: Overall_Status: name: ci/gha_overall_status_linux_arm64 needs: [Smart_CI, Build, Debian_Packages, Samples, ONNX_Runtime, CXX_Unit_Tests, Python_Unit_Tests, CPU_Functional_Tests, - TensorFlow_Models_Tests, PyTorch_Models_Tests, Openvino_tokenizers] + TensorFlow_Models_Tests, PyTorch_Models_Tests, Openvino_tokenizers, TensorFlow_Layer_Tests] if: ${{ always() }} runs-on: ubuntu-latest steps: From 1335be030909df842b92144ef99da680bbdcbe55 Mon Sep 17 00:00:00 2001 From: Steve Yoo Date: Thu, 22 Aug 2024 07:02:41 +0900 Subject: [PATCH 11/35] [GPU] Enable fc 4d for MatMul (#24642) ### Details: - *Enable fc 4d for MatMul to calculate 4x2* ### Tickets: - *132334* --- .../intel_gpu/src/graph/fully_connected.cpp | 4 +- .../test_cases/fully_connected_gpu_test.cpp | 37 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/plugins/intel_gpu/src/graph/fully_connected.cpp b/src/plugins/intel_gpu/src/graph/fully_connected.cpp index cc6d849bcfabe0..b09416585e3fed 100644 --- a/src/plugins/intel_gpu/src/graph/fully_connected.cpp +++ b/src/plugins/intel_gpu/src/graph/fully_connected.cpp @@ -117,7 +117,7 @@ layout fully_connected_inst::calc_output_layout(fully_connected_node const& node feature = std::max({input_layout.spatial(0), input_layout.spatial(1), input_layout.spatial(2)}); } - if (desc->input_size > 3) { + if (desc->input_size > 4) { input_layout.set_partial_shape(reshape_to_2d(input_pshape, feature)); } if (weights_pshape.size() != 2) { @@ -127,6 +127,8 @@ layout fully_connected_inst::calc_output_layout(fully_connected_node const& node auto output_size = tensor(input_layout.batch(), weights_layout.batch(), 1, 1); if (desc->input_size == 3) { output_size = tensor(input_layout.batch(), input_layout.feature(), 1, weights_layout.batch()); + } else if (desc->input_size == 4) { + output_size = tensor(input_layout.batch(), input_layout.feature(), weights_layout.batch(), input_layout.spatial(1)); } format output_format = get_preferred_format(node, impl_param); diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/fully_connected_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/fully_connected_gpu_test.cpp index fa3e375cbecfc5..3f0b43a861873b 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/fully_connected_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/fully_connected_gpu_test.cpp @@ -371,6 +371,43 @@ TEST(fully_connected_gpu, no_biases_fc_i32) { } } +TEST(fully_connected_gpu, no_biases_4d_input) { + // Input : 1x256x256x384 + // Output : 1x256x256x1536 + // Weights: 1536x384x1x1 + + const int32_t input_b = 1, input_f = 256, input_y = 256, input_x = 384, // size of the whole input buffer + weight_b = 1536, weight_f = 384, weight_y = 1, weight_x = 1; // size of the whole weights buffer + + auto& engine = get_test_engine(); + + auto input_prim = engine.allocate_memory({ data_types::f32, format::bfyx, { input_b, input_f, input_x, input_y } }); + auto weights_prim = engine.allocate_memory({ data_types::f32, format::bfyx, { weight_b, weight_f, weight_x, weight_y } }); + + std::vector input_data(input_b * input_f * input_y * input_x, 0); + std::vector weights_data(weight_b * weight_f * weight_y * weight_x, 0); + + set_values(input_prim, std::move(input_data)); + set_values(weights_prim, std::move(weights_data)); + + auto input = input_layout("input", input_prim->get_layout()); + auto w_data = data("weights", weights_prim); + auto fc = fully_connected("fc_prim", input_info("input"), "weights", "", 4, 2); + topology topology; + topology.add(input); + topology.add(w_data); + topology.add(fc); + + network network(engine, topology, get_test_default_config(engine)); + network.set_input_data("input", input_prim); + + auto outputs = network.execute(); + ASSERT_EQ(outputs.begin()->second.get_layout().batch(), input_b); + ASSERT_EQ(outputs.begin()->second.get_layout().feature(), input_f); + ASSERT_EQ(outputs.begin()->second.get_layout().spatial(1), input_y); + ASSERT_EQ(outputs.begin()->second.get_layout().spatial(0), weight_b); +} + TEST(fully_connected_gpu, xb_f32_batch_1) { // Input : 3x1 // Output : 4x1 From abbf944fe17249dac6fcdec0e75c84cf306186ef Mon Sep 17 00:00:00 2001 From: Roman Kazantsev Date: Thu, 22 Aug 2024 03:13:27 +0400 Subject: [PATCH 12/35] [TF FE] Stabilize L2Loss layer tests on all platforms (#26151) **Details:** Stabilize L2Loss layer tests on all platforms **Ticket:** 104863 --------- Signed-off-by: Kazantsev, Roman --- .../tensorflow_tests/test_tf_L2Loss.py | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/tests/layer_tests/tensorflow_tests/test_tf_L2Loss.py b/tests/layer_tests/tensorflow_tests/test_tf_L2Loss.py index e43808fbb688a6..ddd14eb2ae23ea 100644 --- a/tests/layer_tests/tensorflow_tests/test_tf_L2Loss.py +++ b/tests/layer_tests/tensorflow_tests/test_tf_L2Loss.py @@ -1,38 +1,46 @@ # Copyright (C) 2018-2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +import numpy as np import pytest import tensorflow as tf from common.tf_layer_test_class import CommonTFLayerTest +rng = np.random.default_rng(233453) + class TestL2Loss(CommonTFLayerTest): - def create_l2_loss_net(self, input_shape): + def _prepare_input(self, inputs_info): + assert 'input:0' in inputs_info, "Test error: inputs_info must contain `input`" + input_shape = inputs_info['input:0'] + inputs_data = {} + inputs_data['input:0'] = rng.uniform(-2.0, 2.0, input_shape).astype(self.input_type) + return inputs_data + + def create_l2_loss_net(self, input_shape, input_type): + self.input_type = input_type tf.compat.v1.reset_default_graph() # Create the graph and model with tf.compat.v1.Session() as sess: - input = tf.compat.v1.placeholder(tf.float32, input_shape, 'input') + input = tf.compat.v1.placeholder(input_type, input_shape, 'input') tf.raw_ops.L2Loss(t=input, name='l2_loss') tf.compat.v1.global_variables_initializer() - tf_net = sess.graph_def return tf_net, None - test_data_basic = [ - dict(input_shape=[1, 2]), - dict(input_shape=[2, 3, 4]), - ] - - @pytest.mark.parametrize("params", test_data_basic) + @pytest.mark.parametrize("input_shape", [[], [2], [1, 2], [2, 3, 4]]) + @pytest.mark.parametrize("input_type", [np.float16, np.float32, np.float64]) @pytest.mark.precommit @pytest.mark.nightly - def test_l2_loss_basic(self, params, ie_device, precision, ir_version, temp_dir, + def test_l2_loss_basic(self, input_shape, input_type, + ie_device, precision, ir_version, temp_dir, use_legacy_frontend): - if ie_device == 'GPU': - pytest.xfail('104863') - if use_legacy_frontend: - pytest.skip("L2Loss is not supported by legacy FE.") - self._test(*self.create_l2_loss_net(**params), + custom_eps = None + if input_type == np.float16: + custom_eps = 3 * 1e-3 + if ie_device == 'GPU' and input_shape == []: + pytest.skip("150321: Accessing out-of-range dimension on GPU") + self._test(*self.create_l2_loss_net(input_shape, input_type), ie_device, precision, ir_version, temp_dir=temp_dir, - use_legacy_frontend=use_legacy_frontend) + use_legacy_frontend=use_legacy_frontend, custom_eps=custom_eps) From 486fd0b48c2fd38696b9847d715e3c445335e24a Mon Sep 17 00:00:00 2001 From: Zhiyuan Tan <66934674+BHbean@users.noreply.github.com> Date: Thu, 22 Aug 2024 14:59:20 +0800 Subject: [PATCH 13/35] [DOCS] [GSoC] add docs about how to build OV on RISC-V devices (#26056) ### Details: - *add `build_riscv64.md` file giving instructions on how to build OV on RISC-V devices* ### Tickets: - *N/A* --- docs/dev/build.md | 1 + docs/dev/build_riscv64.md | 131 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 docs/dev/build_riscv64.md diff --git a/docs/dev/build.md b/docs/dev/build.md index c70ef73f527fc4..5aca51f0632654 100644 --- a/docs/dev/build.md +++ b/docs/dev/build.md @@ -22,6 +22,7 @@ The articles below provide the basic informations about the process of building * [Raspbian Stretch](./build_raspbian.md) * [Web Assembly](./build_webassembly.md) * [Docker Image](https://github.com/openvinotoolkit/docker_ci/tree/master/dockerfiles/ubuntu18/build_custom) +* [Linux RISC-V](./build_riscv64.md) > **NOTE**: For the details on how to build static OpenVINO, refer to [Building static OpenVINO libraries](static_libaries.md) diff --git a/docs/dev/build_riscv64.md b/docs/dev/build_riscv64.md new file mode 100644 index 00000000000000..75d3782004f4dc --- /dev/null +++ b/docs/dev/build_riscv64.md @@ -0,0 +1,131 @@ +# Cross compile OpenVINOâ„¢ Runtime for RISCV64 systems +This guide shows how to build OpenVINO Runtime for 64-bit RISC-V devices. Due to limited resources, cross compilation is used now for building OpenVINO targeting RISC-V development boards. + +Cross compilation was tested on the following hosts: +- Ubuntu 22.04 (64-bit), x64 + +The software was validated on the following devices: +- [Lichee Pi 4A](https://wiki.sipeed.com/hardware/en/lichee/th1520/lp4a.html) with RVV 0.7.1 +- [Banana Pi BPI-F3](https://www.banana-pi.org/en/banana-pi-sbcs/175.html) with RVV 1.0 + + +## Software requirements + +- [CMake](https://cmake.org/download/) 3.13 or higher +- GCC 7.5 or higher (for non-RVV) / [xuantie-gnu-toolchain](https://github.com/XUANTIE-RV/xuantie-gnu-toolchain) (for RVV) +- Python 3.10 for OpenVINO Runtime Python API + +## How to build +0. Prerequisite: +- For target with RVV - build `xuantie-gnu-toolchain` and `qemu`: + ```sh + git clone https://github.com/XUANTIE-RV/xuantie-gnu-toolchain.git + cd xuantie-gnu-toolchain + ./configure --prefix= + make linux build-qemu -j$(nproc) + ``` +- For target without RVV - build `riscv-gnu-toolchain`: + ```sh + git clone https://github.com/riscv-collab/riscv-gnu-toolchain.git + cd riscv-gnu-toolchain + ./configure --prefix=/opt/riscv + make linux build-qemu -j$(nproc) + ``` + > **NOTE**: The `build-qemu` target is optional, as it is used to build the `qemu` simulator. However, it is recommended to build the `qemu` simulator, since it is much more convenient to validate the software on your host than on your devices. More information can be seen [here](https://github.com/riscv-collab/riscv-gnu-toolchain). + +1. Clone OpenVINO repository and init submodules: + ```sh + git clone --recursive https://github.com/openvinotoolkit/openvino.git + cd openvino + ``` + +2. Install build dependencies using the `install_build_dependencies.sh` script in the + project root folder. + ```sh + sudo ./install_build_dependencies.sh + ``` + +3. Create a build folder: + ```sh + mkdir build && cd build + ``` + +4. To cross compile OpenVINO Runtime for RISC-V devices, run `cmake` with specified `CMAKE_TOOLCHAIN_FILE` and `RISCV_TOOLCHAIN_ROOT`. +- For target with RVV: + ```sh + cmake .. \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX= \ + -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/ \ + -DRISCV_TOOLCHAIN_ROOT= + ``` + > **NOTE**: To build OpenVINO Runtime for different versions of RVV, you just need to specify corresponding toolchain files. For exmaple, you can replace `` with `riscv64-071-thead-gnu.toolchain.cmake` for RVV 0.7.1 and `riscv64-100-thead-gnu.toolchain.cmake` for RVV 1.0 respectively. +- For target without RVV: + ```sh + cmake .. \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX= \ + -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/riscv64-gnu.toolchain.cmake \ + -DRISCV_TOOLCHAIN_ROOT=/opt/riscv + ``` + > **NOTE**: The `riscv-gnu-toolchain` is build as there are essential files used for cross compilation under `/opt/riscv/sysroot`. The latest stable versions of Clang or GCC both support compiling source code into RISC-V instructions, so it is acceptable to choose your preferable compilers by specifying `-DCMAKE_C_COMPILER` and `CMAKE_CXX_COMPILER`. But remember to add the key `-DCMAKE_SYSROOT=/opt/riscv/sysroot`, otherwise many fundamental headers and libs could not be found during cross compilation. + + Then run `make` to build the project: + ```sh + make install -j$(nproc) + ``` + +### (Optional) Build the OpenVINO Runtime Python API +To enable cross-compilation with python, the library `libpython3-dev:riscv64` should be on the host machine. + +When installing packages using the utilities `apt` or `apt-get` the packages are downloaded from apt software repositories. On Ubuntu the apt software repositories are defined in the `/etc/apt/sources.list` file or in separate files under the `/etc/apt/sources.list.d/` directory. Host machine contains host-specific repositories (for example, x86-x64) in these files. + +1. Add riscv64 repositories to download riscv64-specific packages: + ```sh + echo deb [arch=riscv64] http://ports.ubuntu.com/ubuntu-ports/ jammy main >> riscv64-sources.list + echo deb [arch=riscv64] http://ports.ubuntu.com/ubuntu-ports/ jammy universe >> riscv64-sources.list + echo deb [arch=riscv64] http://ports.ubuntu.com/ubuntu-ports/ jammy-updates main >> riscv64-sources.list + echo deb [arch=riscv64] http://ports.ubuntu.com/ubuntu-ports/ jammy-security main >> riscv64-sources.list + mv riscv64-sources.list /etc/apt/sources.list.d/ + dpkg --add-architecture riscv64 + apt-get update -o Dir::Etc::sourcelist=/etc/apt/sources.list.d/riscv64-sources.list + ``` + +2. Install `libpython3-dev:riscv64` using `apt-get`: + ```sh + apt-get install -y --no-install-recommends libpython3-dev:riscv64 + ``` + Create symbolink to allow python to find `riscv64-linux-gnu/python3.10/pyconfig.h` in `/usr/include/python3.10/` (this header is initially stored in `/usr/include/riscv64-linux-gnu/`) + ```sh + ln -s /usr/include/riscv64-linux-gnu/ /usr/include/python3.10/ + ``` + +3. Add the keys `-DENABLE_PYTHON=ON -DENABLE_WHEEL=ON` to cmake command during OpenVINO build. + +> **Note**: Currently only Python 3.10 on Ubuntu 22.04 is verified. So the target device must have Python 3.10 in this case. + +### RISC-V Emulation software +In order to test applications without hardware one can use emulation software. The command line example to launch executable file with riscv64 emulation: +```sh +/bin/qemu-riscv64 -cpu= +``` + +For example, to emulate RVV 0.7.1: +```sh +/bin/qemu-riscv64 -cpu rv64,x-v=true,vext_spec=v0.7.1 +``` + +Or to emulate RVV 1.0: +```sh +/bin/qemu-riscv64 -cpu rv64,x-v=true,vext_spec=v1.0 +``` + +> **Note**: If you are using official `qemu` instead of modified version by Xuantie, you should specify the CPU model with `-cpu rv64,v=true,vext_spec=v1.0` (for `qemu` version greater than `8.0`). + +## See also + + * [OpenVINO README](../../README.md) + * [OpenVINO Developer Documentation](index.md) + * [OpenVINO Get Started](./get_started.md) + * [How to build OpenVINO](build.md) + From 45a6f33e630ff4c15b96fb8ee50d7ef489a563a1 Mon Sep 17 00:00:00 2001 From: Przemyslaw Wysocki Date: Thu, 22 Aug 2024 11:05:47 +0200 Subject: [PATCH 14/35] Allow `numpy==1.26` in MO for Python 3.12 enablement (#26118) ### Details: - `numpy==1.27` is the first version to support Python 3.12 - kaldi, mxnet and caffee will most likely fail with Python 3.12, TBD how to handle it - cc @culhatsker ### Tickets: - CVS-150017 --- tools/mo/requirements_caffe.txt | 2 +- tools/mo/requirements_kaldi.txt | 2 +- tools/mo/requirements_mxnet.txt | 2 +- tools/mo/requirements_onnx.txt | 2 +- tools/mo/requirements_tf.txt | 2 +- tools/mo/requirements_tf2.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tools/mo/requirements_caffe.txt b/tools/mo/requirements_caffe.txt index a8932f1873daaf..2806576890500c 100644 --- a/tools/mo/requirements_caffe.txt +++ b/tools/mo/requirements_caffe.txt @@ -1,5 +1,5 @@ -c ../constraints.txt -numpy>=1.16.6,<1.26 +numpy>=1.16.6,<1.27 networkx protobuf defusedxml diff --git a/tools/mo/requirements_kaldi.txt b/tools/mo/requirements_kaldi.txt index b31121df7bfa84..1365904679bd5f 100644 --- a/tools/mo/requirements_kaldi.txt +++ b/tools/mo/requirements_kaldi.txt @@ -1,5 +1,5 @@ -c ../constraints.txt -numpy>=1.16.6,<1.26 +numpy>=1.16.6,<1.27 networkx defusedxml requests diff --git a/tools/mo/requirements_mxnet.txt b/tools/mo/requirements_mxnet.txt index 8b6561f761b00d..bb4ec290ed5912 100644 --- a/tools/mo/requirements_mxnet.txt +++ b/tools/mo/requirements_mxnet.txt @@ -1,5 +1,5 @@ -c ../constraints.txt -numpy>=1.16.6,<1.24 +numpy>=1.16.6,<1.27 mxnet networkx defusedxml diff --git a/tools/mo/requirements_onnx.txt b/tools/mo/requirements_onnx.txt index f1f52bb03c25fa..28484f314a9d60 100644 --- a/tools/mo/requirements_onnx.txt +++ b/tools/mo/requirements_onnx.txt @@ -1,5 +1,5 @@ -c ../constraints.txt -numpy>=1.16.6,<1.26 +numpy>=1.16.6,<1.27 onnx networkx defusedxml diff --git a/tools/mo/requirements_tf.txt b/tools/mo/requirements_tf.txt index f83e3ee69f4321..abf7c3a5f6400d 100644 --- a/tools/mo/requirements_tf.txt +++ b/tools/mo/requirements_tf.txt @@ -1,7 +1,7 @@ -c ../constraints.txt h5py tensorflow>=1.15.5,<2.17.0 -numpy>=1.16.6,<1.26 +numpy>=1.16.6,<1.27 networkx defusedxml requests diff --git a/tools/mo/requirements_tf2.txt b/tools/mo/requirements_tf2.txt index af80835e3728fe..be055b38cdefa7 100644 --- a/tools/mo/requirements_tf2.txt +++ b/tools/mo/requirements_tf2.txt @@ -1,7 +1,7 @@ -c ../constraints.txt h5py tensorflow>=2.5,<2.17.0 -numpy>=1.16.6,<1.26 +numpy>=1.16.6,<1.27 networkx defusedxml requests From e26f63e397bb4b00540c698b3c7186e25999c36c Mon Sep 17 00:00:00 2001 From: Piotr Kowalczyk Date: Thu, 22 Aug 2024 11:12:31 +0200 Subject: [PATCH 15/35] [GPU]: Added initial support for bitwise shifts with unittests. (#26046) ### Details: - Added support for bitwise shifts in gpu plugin - updated tests ### Tickets: - *CVS-149424* --- .../intel_gpu/plugin/primitives_list.hpp | 4 +- .../include/intel_gpu/primitives/eltwise.hpp | 4 + src/plugins/intel_gpu/src/graph/eltwise.cpp | 16 ++- .../intel_gpu/src/graph/impls/cpu/eltwise.cpp | 8 ++ .../intel_gpu/src/graph/impls/ocl/eltwise.cpp | 8 ++ .../impls/ocl/kernel_selector_helper.cpp | 4 + .../graph/impls/ocl/kernel_selector_helper.h | 5 + .../src/kernel_selector/common_types.h | 2 + .../kernels/eltwise/eltwise_kernel_base.cpp | 8 ++ .../kernels/eltwise/eltwise_kernel_ref.cpp | 2 + .../intel_gpu/src/plugin/ops/constant.cpp | 8 +- .../intel_gpu/src/plugin/ops/eltwise.cpp | 12 +++ .../single_layer_tests/eltwise.cpp | 22 +++++ .../unit/test_cases/eltwise_gpu_test.cpp | 97 +++++++++++++++---- .../include/common_test_utils/test_enums.hpp | 4 +- .../src/node_builders/eltwise.cpp | 6 ++ .../common_test_utils/src/test_enums.cpp | 6 ++ 17 files changed, 192 insertions(+), 24 deletions(-) diff --git a/src/plugins/intel_gpu/include/intel_gpu/plugin/primitives_list.hpp b/src/plugins/intel_gpu/include/intel_gpu/plugin/primitives_list.hpp index e61542a0c50dfc..408f3dfb7cf2d4 100644 --- a/src/plugins/intel_gpu/include/intel_gpu/plugin/primitives_list.hpp +++ b/src/plugins/intel_gpu/include/intel_gpu/plugin/primitives_list.hpp @@ -265,8 +265,10 @@ REGISTER_FACTORY(v12, ScatterElementsUpdate); REGISTER_FACTORY(v13, Multinomial); REGISTER_FACTORY(v13, ScaledDotProductAttention); -// ------------------------------ Supported v14 ops ----------------------------- // +// ------------------------------ Supported v15 ops ----------------------------- // REGISTER_FACTORY(v15, ROIAlignRotated); +REGISTER_FACTORY(v15, BitwiseRightShift); +REGISTER_FACTORY(v15, BitwiseLeftShift); // --------------------------- Supported internal ops --------------------------- // REGISTER_FACTORY(internal, NonMaxSuppressionIEInternal); diff --git a/src/plugins/intel_gpu/include/intel_gpu/primitives/eltwise.hpp b/src/plugins/intel_gpu/include/intel_gpu/primitives/eltwise.hpp index 81612a8c660558..7913435324e89d 100644 --- a/src/plugins/intel_gpu/include/intel_gpu/primitives/eltwise.hpp +++ b/src/plugins/intel_gpu/include/intel_gpu/primitives/eltwise.hpp @@ -54,6 +54,10 @@ enum class eltwise_mode : int32_t { is_inf, /// @brief Eltwise is nan. is_nan, + /// @brief Eltwise bitwise right shift. + right_shift, + /// @brief Eltwise bitwise left shift. + left_shift, }; /// @brief Performs elementwise operations (sum, subtract, max or product) on two input primitives diff --git a/src/plugins/intel_gpu/src/graph/eltwise.cpp b/src/plugins/intel_gpu/src/graph/eltwise.cpp index c55d84e73c6293..0791a44014a079 100644 --- a/src/plugins/intel_gpu/src/graph/eltwise.cpp +++ b/src/plugins/intel_gpu/src/graph/eltwise.cpp @@ -79,7 +79,9 @@ layout eltwise_inst::calc_output_layout(eltwise_node const& node, kernel_impl_pa eltwise_mode::floor_mod, eltwise_mode::logic_and, eltwise_mode::logic_or, - eltwise_mode::logic_xor}; + eltwise_mode::logic_xor, + eltwise_mode::right_shift, + eltwise_mode::left_shift}; if (std::find(eltwise_int_modes.begin(), eltwise_int_modes.end(), mode) == eltwise_int_modes.end()) CLDNN_ERROR_MESSAGE(desc->id, "Requested eltwise mode is not supported for integer types."); } @@ -177,7 +179,9 @@ std::vector eltwise_inst::calc_output_layouts(eltwise_node const& /*node eltwise_mode::floor_mod, eltwise_mode::logic_and, eltwise_mode::logic_or, - eltwise_mode::logic_xor}; + eltwise_mode::logic_xor, + eltwise_mode::right_shift, + eltwise_mode::left_shift}; OPENVINO_ASSERT((std::find(eltwise_int_modes.begin(), eltwise_int_modes.end(), mode) != eltwise_int_modes.end()), desc->id + "Requested eltwise mode is not supported for integer types."); @@ -305,6 +309,12 @@ std::string eltwise_inst::to_string(eltwise_node const& node) { case eltwise_mode::is_nan: str_mode = "is_nan"; break; + case eltwise_mode::right_shift: + str_mode = "right_shift"; + break; + case eltwise_mode::left_shift: + str_mode = "left_shift"; + break; default: str_mode = "not supported mode"; break; @@ -428,6 +438,8 @@ void eltwise_inst::check_inputs_count(eltwise_node const& node) { case eltwise_mode::squared_diff: case eltwise_mode::pow: case eltwise_mode::floor_mod: + case eltwise_mode::right_shift: + case eltwise_mode::left_shift: OPENVINO_ASSERT(inputs_number == 2, "Node id: ", node.id(), ". Invalid eltwise inputs number (should be equal to 2). Actual: ", inputs_number); break; diff --git a/src/plugins/intel_gpu/src/graph/impls/cpu/eltwise.cpp b/src/plugins/intel_gpu/src/graph/impls/cpu/eltwise.cpp index eb10f340d2656b..8b95cfc626fa44 100644 --- a/src/plugins/intel_gpu/src/graph/impls/cpu/eltwise.cpp +++ b/src/plugins/intel_gpu/src/graph/impls/cpu/eltwise.cpp @@ -29,6 +29,8 @@ #include "openvino/op/is_finite.hpp" #include "openvino/op/is_inf.hpp" #include "openvino/op/is_nan.hpp" +#include "openvino/op/bitwise_right_shift.hpp" +#include "openvino/op/bitwise_left_shift.hpp" namespace cldnn { namespace cpu { @@ -166,6 +168,12 @@ struct eltwise_impl : public typed_primitive_impl { case eltwise_mode::is_nan: op = std::make_shared(); break; + case eltwise_mode::right_shift: + op = std::make_shared(); + break; + case eltwise_mode::left_shift: + op = std::make_shared(); + break; default: OPENVINO_THROW("[GPU] Couldn't create eltwise operation: unsupported eltwise operation (", static_cast(mode), ")"); } diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl/eltwise.cpp b/src/plugins/intel_gpu/src/graph/impls/ocl/eltwise.cpp index 1b4a63a1c240ad..0e85aa7e294d21 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl/eltwise.cpp +++ b/src/plugins/intel_gpu/src/graph/impls/ocl/eltwise.cpp @@ -171,6 +171,7 @@ attach_eltwise_impl::attach_eltwise_impl() { data_types::i8, data_types::u8, data_types::i32, + data_types::u32, data_types::i64 }; @@ -193,12 +194,14 @@ attach_eltwise_impl::attach_eltwise_impl() { std::make_tuple(data_types::f16, format::yxfb), std::make_tuple(data_types::i8, format::yxfb), std::make_tuple(data_types::u8, format::yxfb), + std::make_tuple(data_types::u32, format::yxfb), std::make_tuple(data_types::i32, format::yxfb), std::make_tuple(data_types::i64, format::yxfb), std::make_tuple(data_types::f32, format::bfyx), std::make_tuple(data_types::f16, format::bfyx), std::make_tuple(data_types::u8, format::bfyx), + std::make_tuple(data_types::u32, format::bfyx), std::make_tuple(data_types::i8, format::bfyx), std::make_tuple(data_types::i32, format::bfyx), std::make_tuple(data_types::i64, format::bfyx), @@ -207,6 +210,7 @@ attach_eltwise_impl::attach_eltwise_impl() { std::make_tuple(data_types::f16, format::byxf), std::make_tuple(data_types::i8, format::byxf), std::make_tuple(data_types::u8, format::byxf), + std::make_tuple(data_types::u32, format::byxf), std::make_tuple(data_types::i32, format::byxf), std::make_tuple(data_types::i64, format::byxf), @@ -219,6 +223,7 @@ attach_eltwise_impl::attach_eltwise_impl() { std::make_tuple(data_types::f16, format::bfzyx), std::make_tuple(data_types::i8, format::bfzyx), std::make_tuple(data_types::u8, format::bfzyx), + std::make_tuple(data_types::u32, format::bfzyx), std::make_tuple(data_types::i32, format::bfzyx), std::make_tuple(data_types::i64, format::bfzyx), @@ -226,6 +231,7 @@ attach_eltwise_impl::attach_eltwise_impl() { std::make_tuple(data_types::f16, format::bfwzyx), std::make_tuple(data_types::i8, format::bfwzyx), std::make_tuple(data_types::u8, format::bfwzyx), + std::make_tuple(data_types::u32, format::bfwzyx), std::make_tuple(data_types::i32, format::bfwzyx), std::make_tuple(data_types::i64, format::bfwzyx), @@ -233,6 +239,7 @@ attach_eltwise_impl::attach_eltwise_impl() { std::make_tuple(data_types::f16, format::bfuwzyx), std::make_tuple(data_types::i8, format::bfuwzyx), std::make_tuple(data_types::u8, format::bfuwzyx), + std::make_tuple(data_types::u32, format::bfuwzyx), std::make_tuple(data_types::i32, format::bfuwzyx), std::make_tuple(data_types::i64, format::bfuwzyx), @@ -240,6 +247,7 @@ attach_eltwise_impl::attach_eltwise_impl() { std::make_tuple(data_types::f16, format::bfvuwzyx), std::make_tuple(data_types::i8, format::bfvuwzyx), std::make_tuple(data_types::u8, format::bfvuwzyx), + std::make_tuple(data_types::u32, format::bfvuwzyx), std::make_tuple(data_types::i32, format::bfvuwzyx), std::make_tuple(data_types::i64, format::bfvuwzyx), diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl/kernel_selector_helper.cpp b/src/plugins/intel_gpu/src/graph/impls/ocl/kernel_selector_helper.cpp index 2ebcebd3b0b48f..1e6d06755a7a86 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl/kernel_selector_helper.cpp +++ b/src/plugins/intel_gpu/src/graph/impls/ocl/kernel_selector_helper.cpp @@ -173,6 +173,8 @@ kernel_selector::data_type to_data_type(data_types dt) { return kernel_selector::data_type::UINT8; case cldnn::data_types::i32: return kernel_selector::data_type::INT32; + case cldnn::data_types::u32: + return kernel_selector::data_type::UINT32; case cldnn::data_types::i64: return kernel_selector::data_type::INT64; case cldnn::data_types::f16: @@ -198,6 +200,8 @@ data_types from_data_type(kernel_selector::data_type dt) { return cldnn::data_types::u8; case kernel_selector::data_type::INT32: return cldnn::data_types::i32; + case kernel_selector::data_type::UINT32: + return cldnn::data_types::u32; case kernel_selector::data_type::INT64: return cldnn::data_types::i64; case kernel_selector::data_type::F16: diff --git a/src/plugins/intel_gpu/src/graph/impls/ocl/kernel_selector_helper.h b/src/plugins/intel_gpu/src/graph/impls/ocl/kernel_selector_helper.h index e17fb1c7045c17..59a7e8e9627b14 100644 --- a/src/plugins/intel_gpu/src/graph/impls/ocl/kernel_selector_helper.h +++ b/src/plugins/intel_gpu/src/graph/impls/ocl/kernel_selector_helper.h @@ -200,7 +200,12 @@ inline kernel_selector::eltwise_mode convert_to_eltwise_mode(eltwise_mode mode) return kernel_selector::eltwise_mode::IS_INF; case eltwise_mode::is_nan: return kernel_selector::eltwise_mode::IS_NAN; + case eltwise_mode::right_shift: + return kernel_selector::eltwise_mode::RIGHT_SHIFT; + case eltwise_mode::left_shift: + return kernel_selector::eltwise_mode::LEFT_SHIFT; default: + OPENVINO_ASSERT(false, "Unsupported eltwise mode!"); return kernel_selector::eltwise_mode::ADD; } } diff --git a/src/plugins/intel_gpu/src/kernel_selector/common_types.h b/src/plugins/intel_gpu/src/kernel_selector/common_types.h index bf96ff16aa4c8f..12e685e0b091f6 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/common_types.h +++ b/src/plugins/intel_gpu/src/kernel_selector/common_types.h @@ -304,6 +304,8 @@ enum class EltwiseMode { IS_FINITE, IS_INF, IS_NAN, + RIGHT_SHIFT, + LEFT_SHIFT, }; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/eltwise/eltwise_kernel_base.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/eltwise/eltwise_kernel_base.cpp index 94b6ac46e9362f..da46e89002e2c4 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/eltwise/eltwise_kernel_base.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/eltwise/eltwise_kernel_base.cpp @@ -49,6 +49,8 @@ uint32_t GetNumberOfInputs(EltwiseMode m) { case EltwiseMode::LOGIC_XOR: case EltwiseMode::SQUARED_DIFF: case EltwiseMode::FLOOR_MOD: + case EltwiseMode::RIGHT_SHIFT: + case EltwiseMode::LEFT_SHIFT: return 2; case EltwiseMode::SQRT: case EltwiseMode::RSQRT: @@ -313,6 +315,12 @@ JitConstants EltwiseKernelBase::GetOperationsJitConstants(const eltwise_params& case EltwiseMode::IS_NAN: op += "(isnan(" + input0_str + "))"; break; + case EltwiseMode::RIGHT_SHIFT: + op += "(" + input0_str + " >> " + input1_str + ")"; + break; + case EltwiseMode::LEFT_SHIFT: + op += "(" + input0_str + " << " + input1_str + ")"; + break; default: break; } diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/eltwise/eltwise_kernel_ref.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/eltwise/eltwise_kernel_ref.cpp index 1f598759fb1580..71f72d68089f25 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/eltwise/eltwise_kernel_ref.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/eltwise/eltwise_kernel_ref.cpp @@ -14,12 +14,14 @@ ParamsKey EltwiseKernelRef::GetSupportedKey() const { k.EnableInputDataType(Datatype::INT8); k.EnableInputDataType(Datatype::UINT8); k.EnableInputDataType(Datatype::INT32); + k.EnableInputDataType(Datatype::UINT32); k.EnableInputDataType(Datatype::INT64); k.EnableOutputDataType(Datatype::F16); k.EnableOutputDataType(Datatype::F32); k.EnableOutputDataType(Datatype::INT8); k.EnableOutputDataType(Datatype::UINT8); k.EnableOutputDataType(Datatype::INT32); + k.EnableOutputDataType(Datatype::UINT32); k.EnableOutputDataType(Datatype::INT64); k.EnableDifferentTypes(); k.EnableAllInputLayout(); diff --git a/src/plugins/intel_gpu/src/plugin/ops/constant.cpp b/src/plugins/intel_gpu/src/plugin/ops/constant.cpp index ec48aff9736a9a..f87c2b39848d09 100644 --- a/src/plugins/intel_gpu/src/plugin/ops/constant.cpp +++ b/src/plugins/intel_gpu/src/plugin/ops/constant.cpp @@ -22,6 +22,7 @@ #include "openvino/op/util/op_types.hpp" #include "openvino/op/loop.hpp" #include "openvino/op/tensor_iterator.hpp" +#include "openvino/op/util/binary_elementwise_bitwise.hpp" #include "intel_gpu/primitives/data.hpp" #include "intel_gpu/runtime/debug_configuration.hpp" @@ -126,6 +127,10 @@ static void create_data(ProgramBuilder& p, const ov::Shape& const_shape, const s } } +static bool is_btiwise(Node* node) { + return dynamic_cast(node) != nullptr; +} + static void CreateConstantOp(ProgramBuilder& p, const std::shared_ptr& op) { ov::Shape constDims = op->get_shape(); auto constUsers = op->get_output_target_inputs(0); @@ -137,7 +142,8 @@ static void CreateConstantOp(ProgramBuilder& p, const std::shared_ptr bool { if (ov::op::util::is_binary_elementwise_arithmetic(op) || ov::op::util::is_binary_elementwise_logical(op) || - ov::op::util::is_binary_elementwise_comparison(op)) { + ov::op::util::is_binary_elementwise_comparison(op) || + is_btiwise(op)) { return true; } else { return false; diff --git a/src/plugins/intel_gpu/src/plugin/ops/eltwise.cpp b/src/plugins/intel_gpu/src/plugin/ops/eltwise.cpp index 0c70da0938da59..2866e9151e214d 100644 --- a/src/plugins/intel_gpu/src/plugin/ops/eltwise.cpp +++ b/src/plugins/intel_gpu/src/plugin/ops/eltwise.cpp @@ -29,6 +29,8 @@ #include "openvino/op/xor.hpp" #include "openvino/op/power.hpp" #include "openvino/op/floor_mod.hpp" +#include "openvino/op/bitwise_right_shift.hpp" +#include "openvino/op/bitwise_left_shift.hpp" #include "intel_gpu/primitives/activation.hpp" #include "intel_gpu/primitives/eltwise.hpp" @@ -198,6 +200,14 @@ static void CreateIsNaNOp(ProgramBuilder& p, const std::shared_ptr& op) { + CreateElementwiseOp(p, op, cldnn::eltwise_mode::right_shift); +} + +static void CreateBitwiseLeftShiftOp(ProgramBuilder& p, const std::shared_ptr& op) { + CreateElementwiseOp(p, op, cldnn::eltwise_mode::left_shift); +} + REGISTER_FACTORY_IMPL(v1, Add); REGISTER_FACTORY_IMPL(v1, Multiply); REGISTER_FACTORY_IMPL(v1, Maximum); @@ -220,6 +230,8 @@ REGISTER_FACTORY_IMPL(v1, Mod); REGISTER_FACTORY_IMPL(v10, IsFinite); REGISTER_FACTORY_IMPL(v10, IsInf); REGISTER_FACTORY_IMPL(v10, IsNaN); +REGISTER_FACTORY_IMPL(v15, BitwiseRightShift); +REGISTER_FACTORY_IMPL(v15, BitwiseLeftShift); } // namespace intel_gpu } // namespace ov diff --git a/src/plugins/intel_gpu/tests/functional/shared_tests_instances/single_layer_tests/eltwise.cpp b/src/plugins/intel_gpu/tests/functional/shared_tests_instances/single_layer_tests/eltwise.cpp index 526bde9495661d..c5447983ba9492 100644 --- a/src/plugins/intel_gpu/tests/functional/shared_tests_instances/single_layer_tests/eltwise.cpp +++ b/src/plugins/intel_gpu/tests/functional/shared_tests_instances/single_layer_tests/eltwise.cpp @@ -66,8 +66,30 @@ std::vector eltwiseOpTypes = { EltwiseTypes::MOD }; +std::vector smoke_intOnly_eltwiseOpTypes = { + EltwiseTypes::RIGHT_SHIFT +}; + +std::vector intOnly_netPrecisions = { + ov::element::i32 +}; + ov::AnyMap additional_config = {}; +INSTANTIATE_TEST_SUITE_P( + smoke_intOnly_CompareWithRefs, + EltwiseLayerTest, + ::testing::Combine(::testing::ValuesIn(ov::test::static_shapes_to_test_representation(inShapes)), + ::testing::ValuesIn(smoke_intOnly_eltwiseOpTypes), + ::testing::ValuesIn(secondaryInputTypes), + ::testing::ValuesIn(opTypes), + ::testing::ValuesIn(intOnly_netPrecisions), + ::testing::Values(ov::element::undefined), + ::testing::Values(ov::element::undefined), + ::testing::Values(ov::test::utils::DEVICE_GPU), + ::testing::Values(additional_config)), + EltwiseLayerTest::getTestCaseName); + INSTANTIATE_TEST_SUITE_P( smoke_CompareWithRefs, EltwiseLayerTest, diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/eltwise_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/eltwise_gpu_test.cpp index 690e570e42ec68..1ae82b70ff1d41 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/eltwise_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/eltwise_gpu_test.cpp @@ -170,8 +170,8 @@ void run_eltwise_generic_test(cldnn::eltwise_mode mode) { } -template -int8_t eltwise_bool_execute(cldnn::eltwise_mode mode, T x, T y) { +template +TOut eltwise_int_execute(cldnn::eltwise_mode mode, T x, T y) { switch (mode) { case eltwise_mode::eq: return x == y; @@ -189,13 +189,17 @@ int8_t eltwise_bool_execute(cldnn::eltwise_mode mode, T x, T y) { return x && y; case eltwise_mode::logic_or: return x || y; + case eltwise_mode::right_shift: + return x >> y; + case eltwise_mode::left_shift: + return x << y; default: - return (int8_t)0; + return (TOut)0; } } -template -VVVVF eltwise_bool_reference(VVVVF &input1, VVVVF &input2, +template +VVVVF eltwise_int_reference(VVVVF &input1, VVVVF &input2, cldnn::eltwise_mode mode, int input_padding_y = 0, int input_padding_x = 0, int output_padding_y = 0, int output_padding_x = 0) { @@ -206,14 +210,14 @@ VVVVF eltwise_bool_reference(VVVVF &input1, VVVVF &input2, size_t output_f = input1[0].size(); size_t output_y = input1[0][0].size() + 2 * padding_y; size_t output_x = input1[0][0][0].size() + 2 * padding_x; - VVVVF output(output_b, VVVF(output_f, VVF(output_y, VF(output_x)))); + VVVVF output(output_b, VVVF(output_f, VVF(output_y, VF(output_x)))); T res; for (size_t b = 0; b < output_b; ++b) { for (size_t f = 0; f < output_f; ++f) { for (size_t y = 0; y < input1[0][0].size(); ++y) { for (size_t x = 0; x < input1[0][0][0].size(); ++x) { - res = eltwise_bool_execute(mode, input1[b][f][y][x], input2[b][f][y][x]); + res = eltwise_int_execute(mode, input1[b][f][y][x], input2[b][f][y][x]); output[b][f][y + padding_y][x + padding_x] = res; } } @@ -222,14 +226,28 @@ VVVVF eltwise_bool_reference(VVVVF &input1, VVVVF &input2, return output; } -template -void generic_eltwise_bool_test(cldnn::format test_input_fmt, int input_b, int input_f, int input_y, int input_x, cldnn::eltwise_mode mode, - int input_padding_y, int input_padding_x, int output_padding_y, int output_padding_x) { +template +void generic_eltwise_int_test(cldnn::format test_input_fmt, + int input_b, + int input_f, + int input_y, + int input_x, + cldnn::eltwise_mode mode, + int input_padding_y, + int input_padding_x, + int output_padding_y, + int output_padding_x, + int input1_min_val, + int input1_max_val, + int input2_min_val, + int input2_max_val) { + static_assert(std::is_integral::value, "T must be an integral type"); + static_assert(std::is_integral::value, "TOut must be an integral type"); + tests::random_generator rg(GET_SUITE_NAME); - int min_random = -2, max_random = 2; - VVVVF input1_rnd = rg.generate_random_4d(input_b, input_f, input_y, input_x, min_random, max_random); - VVVVF input2_rnd = rg.generate_random_4d(input_b, input_f, input_y, input_x, min_random, max_random); + VVVVF input1_rnd = rg.generate_random_4d(input_b, input_f, input_y, input_x, input1_min_val, input1_max_val); + VVVVF input2_rnd = rg.generate_random_4d(input_b, input_f, input_y, input_x, input2_min_val, input2_max_val); VF input1_rnd_vec = flatten_4d(test_input_fmt, input1_rnd); VF input2_rnd_vec = flatten_4d(test_input_fmt, input2_rnd); @@ -257,9 +275,9 @@ void generic_eltwise_bool_test(cldnn::format test_input_fmt, int input_b, int in auto output_memory = outputs.at("eltwise").get_memory(); auto output_layout = output_memory->get_layout(); - cldnn::mem_lock output_ptr(output_memory, get_test_stream()); + cldnn::mem_lock output_ptr(output_memory, get_test_stream()); - VVVVF output_cpu = eltwise_bool_reference(input1_rnd, input2_rnd, mode, input_padding_y, input_padding_x, output_padding_y, output_padding_x); + VVVVF output_cpu = eltwise_int_reference(input1_rnd, input2_rnd, mode, input_padding_y, input_padding_x, output_padding_y, output_padding_x); ASSERT_EQ(output_layout.format.value, test_input_fmt.value); auto output_tensor = output_layout.get_padded_dims(); int x_size = output_tensor[3]; @@ -273,9 +291,11 @@ void generic_eltwise_bool_test(cldnn::format test_input_fmt, int input_b, int in ASSERT_EQ(b_size, (int)output_cpu.size()); bool test_is_correct = true; - VF output_cpu_vec = flatten_4d(test_input_fmt, output_cpu); + VF output_cpu_vec = flatten_4d(test_input_fmt, output_cpu); for (size_t i = 0; i < output_cpu_vec.size(); ++i) { - if (output_cpu_vec[i] != output_ptr[i]) { + const TOut cpu_val = output_cpu_vec[i]; + const TOut gpu_val = output_ptr[i]; + if (cpu_val != gpu_val) { test_is_correct = false; break; } @@ -299,8 +319,39 @@ void run_eltwise_bool_generic_test(cldnn::eltwise_mode mode) cldnn::format test_inputs_fmt = cldnn::format::bfyx; std::pair input_size = { 227, 227 }; - generic_eltwise_bool_test(test_inputs_fmt, 1, 1, input_size.first, input_size.second, mode, 0, 0, 0, 0); - generic_eltwise_bool_test(test_inputs_fmt, 1, 1, input_size.first, input_size.second, mode, 0, 0, 0, 0); + generic_eltwise_int_test(test_inputs_fmt, 1, 1, input_size.first, input_size.second, mode, 0, 0, 0, 0, -2, 2, -2, 2); + generic_eltwise_int_test(test_inputs_fmt, 1, 1, input_size.first, input_size.second, mode, 0, 0, 0, 0, -2, 2, -2, 2); +} + +void run_eltwise_int_shift_generic_test(cldnn::eltwise_mode mode) { + OPENVINO_ASSERT(mode == eltwise_mode::right_shift || mode == eltwise_mode::left_shift, + "Only right_shift amd left_shift mode is supported for this test"); + cldnn::format test_inputs_fmt = cldnn::format::bfyx; + const int dim_size = 227; + +#define ELTWISE_INT_TEST_CASES(type) \ + generic_eltwise_int_test(test_inputs_fmt, \ + 1, \ + 1, \ + dim_size, \ + dim_size, \ + mode, \ + 0, \ + 0, \ + 0, \ + 0, \ + 0, \ + static_cast(std::numeric_limits::max()) / 2, \ + 0, \ + ((sizeof(type) * 8) - 1) / 2); + + ELTWISE_INT_TEST_CASES(int8_t); + ELTWISE_INT_TEST_CASES(uint8_t); + ELTWISE_INT_TEST_CASES(int32_t); + ELTWISE_INT_TEST_CASES(uint32_t); + ELTWISE_INT_TEST_CASES(int64_t); + +#undef ELTWISE_INT_TEST_CASES } } // namespace @@ -3894,6 +3945,14 @@ TEST(eltwise_gpu_bool, eltwise_or) { run_eltwise_bool_generic_test(cldnn::eltwise_mode::logic_or); } +TEST(eltwise_gpu, eltwise_right_shift) { + run_eltwise_int_shift_generic_test(cldnn::eltwise_mode::right_shift); +} + +TEST(eltwise_gpu, eltwise_left_shift) { + run_eltwise_int_shift_generic_test(cldnn::eltwise_mode::left_shift); +} + TEST(eltwise_gpu, eltwise_div) { run_eltwise_generic_test(cldnn::eltwise_mode::div); } diff --git a/src/tests/test_utils/common_test_utils/include/common_test_utils/test_enums.hpp b/src/tests/test_utils/common_test_utils/include/common_test_utils/test_enums.hpp index 7b8e4b8834f854..8a7fe4fec02874 100644 --- a/src/tests/test_utils/common_test_utils/include/common_test_utils/test_enums.hpp +++ b/src/tests/test_utils/common_test_utils/include/common_test_utils/test_enums.hpp @@ -60,7 +60,9 @@ enum EltwiseTypes { BITWISE_AND, BITWISE_NOT, BITWISE_OR, - BITWISE_XOR + BITWISE_XOR, + RIGHT_SHIFT, + LEFT_SHIFT }; enum SqueezeOpType { diff --git a/src/tests/test_utils/common_test_utils/src/node_builders/eltwise.cpp b/src/tests/test_utils/common_test_utils/src/node_builders/eltwise.cpp index 262ceb30187137..b5c792aa26e021 100644 --- a/src/tests/test_utils/common_test_utils/src/node_builders/eltwise.cpp +++ b/src/tests/test_utils/common_test_utils/src/node_builders/eltwise.cpp @@ -6,8 +6,10 @@ #include "openvino/op/add.hpp" #include "openvino/op/bitwise_and.hpp" +#include "openvino/op/bitwise_left_shift.hpp" #include "openvino/op/bitwise_not.hpp" #include "openvino/op/bitwise_or.hpp" +#include "openvino/op/bitwise_right_shift.hpp" #include "openvino/op/bitwise_xor.hpp" #include "openvino/op/divide.hpp" #include "openvino/op/erf.hpp" @@ -51,6 +53,10 @@ std::shared_ptr make_eltwise(const ov::Output& in0, return std::make_shared(in0, in1); case ov::test::utils::EltwiseTypes::BITWISE_XOR: return std::make_shared(in0, in1); + case ov::test::utils::EltwiseTypes::RIGHT_SHIFT: + return std::make_shared(in0, in1); + case ov::test::utils::EltwiseTypes::LEFT_SHIFT: + return std::make_shared(in0, in1); default: { OPENVINO_THROW("Incorrect type of Eltwise operation"); } diff --git a/src/tests/test_utils/common_test_utils/src/test_enums.cpp b/src/tests/test_utils/common_test_utils/src/test_enums.cpp index 83ab16fc34d2d0..219aeec1e9fa50 100644 --- a/src/tests/test_utils/common_test_utils/src/test_enums.cpp +++ b/src/tests/test_utils/common_test_utils/src/test_enums.cpp @@ -82,6 +82,12 @@ std::ostream& operator<<(std::ostream& os, const ov::test::utils::EltwiseTypes t case ov::test::utils::EltwiseTypes::BITWISE_XOR: os << "BitwiseXor"; break; + case ov::test::utils::EltwiseTypes::RIGHT_SHIFT: + os << "BitwiseRightShift"; + break; + case ov::test::utils::EltwiseTypes::LEFT_SHIFT: + os << "BitwiseLeftShift"; + break; default: throw std::runtime_error("NOT_SUPPORTED_OP_TYPE"); } From 49601012d05a9e5bb5b36ce42e96668a80d2c893 Mon Sep 17 00:00:00 2001 From: Mingyu Kim Date: Thu, 22 Aug 2024 19:28:34 +0900 Subject: [PATCH 16/35] [GPU] propagate fc output type for dynamic quantization case (#26133) ### Details: - When FC output type is undefined, propagate the original input type - Otherwise, dynamic_quantized type is propagated ### Tickets: - 149663 --- .../dynamic_quantize_fully_connected.cpp | 7 +++- .../transformations/fc_horizontal_fusion.cpp | 13 +++++- .../dynamic/dynamic_fc_horizontal_fusion.cpp | 42 ++++++++++++++++--- .../dynamic/matmul_weights_decompression.cpp | 6 +-- 4 files changed, 56 insertions(+), 12 deletions(-) diff --git a/src/plugins/intel_gpu/src/plugin/transformations/dynamic_quantize_fully_connected.cpp b/src/plugins/intel_gpu/src/plugin/transformations/dynamic_quantize_fully_connected.cpp index a2e4a1a38983c8..eb16213bcb936c 100644 --- a/src/plugins/intel_gpu/src/plugin/transformations/dynamic_quantize_fully_connected.cpp +++ b/src/plugins/intel_gpu/src/plugin/transformations/dynamic_quantize_fully_connected.cpp @@ -64,13 +64,18 @@ DynamicQuantizeFullyConnected::DynamicQuantizeFullyConnected(uint64_t group_size auto dyn_quan = std::make_shared(m_data, shape_group_size, element::f16); auto optional_w_zp = m_fc->get_input_size() > 4 ? m_fc->get_input_node_shared_ptr(4) : std::make_shared(); + auto output_type = m_fc->get_output_type(); + if (output_type == ov::element::undefined) + output_type = m_fc->get_input_element_type(0); + auto new_fc = std::make_shared(dyn_quan->output(0), m_fc->get_input_node_shared_ptr(1), m_fc->get_input_node_shared_ptr(2), m_fc->get_input_node_shared_ptr(3), optional_w_zp, dyn_quan->output(1), - m_fc->get_output_type()); + output_type); + ov::replace_node(m_fc, new_fc); new_fc->set_friendly_name(m_fc->get_friendly_name()); diff --git a/src/plugins/intel_gpu/src/plugin/transformations/fc_horizontal_fusion.cpp b/src/plugins/intel_gpu/src/plugin/transformations/fc_horizontal_fusion.cpp index 14bb5596b1cc12..a5ec6c0060ef81 100644 --- a/src/plugins/intel_gpu/src/plugin/transformations/fc_horizontal_fusion.cpp +++ b/src/plugins/intel_gpu/src/plugin/transformations/fc_horizontal_fusion.cpp @@ -169,9 +169,18 @@ FullyConnectedHorizontalFusion::FullyConnectedHorizontalFusion() { // Create new fc with merged weights, bias, scale, zp std::shared_ptr new_fc; if (fused_zps) - new_fc = std::make_shared(input_node, fused_weight, fused_bias, fused_scale, fused_zps); + new_fc = std::make_shared(input_node, + fused_weight, + fused_bias, + fused_scale, + fused_zps, + fc_nodes[0]->get_output_type()); else - new_fc = std::make_shared(input_node, fused_weight, fused_bias, fused_scale); + new_fc = std::make_shared(input_node, + fused_weight, + fused_bias, + fused_scale, + fc_nodes[0]->get_output_type()); auto new_fc_name = fc_nodes[0]->get_friendly_name() + "_fused"; new_fc->set_friendly_name(new_fc_name); diff --git a/src/plugins/intel_gpu/tests/functional/subgraph_tests/dynamic/dynamic_fc_horizontal_fusion.cpp b/src/plugins/intel_gpu/tests/functional/subgraph_tests/dynamic/dynamic_fc_horizontal_fusion.cpp index 4aad7f554a0dfa..47dfb8ab3b80da 100644 --- a/src/plugins/intel_gpu/tests/functional/subgraph_tests/dynamic/dynamic_fc_horizontal_fusion.cpp +++ b/src/plugins/intel_gpu/tests/functional/subgraph_tests/dynamic/dynamic_fc_horizontal_fusion.cpp @@ -38,7 +38,9 @@ using FullyConnectedHorizontalFusionParams = std::tuple; // has biahs + bool, // has bias + uint64_t // dynamic_quantization_group_size + >; class FullyConnectedHorizontalFusion : public testing::WithParamInterface, @@ -53,6 +55,7 @@ class FullyConnectedHorizontalFusion : public testing::WithParamInterface& m_pool_gpr_idxs; + Reg64 m_aux_gpr_idx {}; + bool m_is_preserved = false; +}; +} // namespace + /* ================== jit_loop_begin_emitter ====================== */ jit_loop_begin_emitter::jit_loop_begin_emitter(dnnl::impl::cpu::x64::jit_generator* h, dnnl::impl::cpu::x64::cpu_isa_t isa, @@ -30,12 +76,6 @@ jit_loop_begin_emitter::jit_loop_begin_emitter(dnnl::impl::cpu::x64::jit_generat in_out_type_ = emitter_in_out_map::gpr_to_gpr; } -size_t jit_loop_begin_emitter::aux_gprs_count() const { - // We should have aux GPR to store Loop arguments from `runtime_args` - // where we will take all needed information about the current loop: work amount - return is_work_amount_dynamic ? 1 : 0; -} - void jit_loop_begin_emitter::validate_arguments(const std::vector &in, const std::vector &out) const { OV_CPU_JIT_EMITTER_ASSERT(in.empty(), "Invalid inputs size: expected 0 got " + std::to_string(in.size())); // Note: the only expected output is work amount register (communicated to jit_loop_end_emitter) @@ -59,10 +99,10 @@ void jit_loop_begin_emitter::emit_impl(const std::vector& in, const std: Reg64 reg_work_amount = Reg64(static_cast(out.back())); if (is_work_amount_dynamic) { - Reg64 reg_runtime_params = abi_param1; // defined by jit_kernel_emitter - Reg64 reg_loop_args_ptr = Reg64(static_cast(aux_gpr_idxs[0])); + jit_aux_gpr_holder gpr_holder(h, aux_gpr_idxs, out); // loop_begin has only output registers + Reg64 reg_loop_args_ptr = gpr_holder.get_reg(); const auto id_offset = loop_id * sizeof(jit_snippets_call_args::loop_args_t); - h->mov(reg_loop_args_ptr, h->ptr[reg_runtime_params + GET_OFF(loop_args)]); + h->mov(reg_loop_args_ptr, h->ptr[abi_param1 + GET_OFF(loop_args)]); h->mov(reg_work_amount, h->ptr[reg_loop_args_ptr + id_offset + GET_OFF_LOOP_ARGS(m_work_amount)]); } else { h->mov(reg_work_amount, work_amount); @@ -141,37 +181,37 @@ void jit_loop_end_emitter::emit_code(const std::vector &in, const std::v jit_emitter::emit_code(in, out, pool_vec_idxs, pool_gpr_idxs); } -size_t jit_loop_end_emitter::aux_gprs_count() const { - // We should have aux GPR to store Loop arguments from `runtime_args` - // where we will take all needed information about the current loop: data pointer shifts - return are_ptr_shifts_dynamic ? 1 : 0; -} - void jit_loop_end_emitter::emit_impl(const std::vector& in, const std::vector& out) const { std::vector data_ptr_reg_idxs; // the last input is actually a work_amount reg data_ptr_reg_idxs.reserve(num_inputs + num_outputs); std::copy(in.begin(), in.end() - 1, std::back_inserter(data_ptr_reg_idxs)); - const auto id_offset = loop_id * sizeof(jit_snippets_call_args::loop_args_t); - Reg64 reg_increments = are_ptr_shifts_dynamic ? Reg64(static_cast(aux_gpr_idxs[0])) : Reg64(); - auto apply_increments = [&](bool use_runtime_args, size_t field_offset, const std::vector& increments, size_t scale) { - if (use_runtime_args) { - Reg64 reg_runtime_params = abi_param1; /* defined by jit_kernel_emitter */ - h->mov(reg_increments, h->ptr[reg_runtime_params + GET_OFF(loop_args)]); - h->mov(reg_increments, h->ptr[reg_increments + id_offset + field_offset]); - } - for (size_t idx = 0; idx < data_ptr_reg_idxs.size(); idx++) { - const auto& increment = increments[idx]; - if (is_incremented[idx] && increment != 0) { - if (ov::snippets::utils::is_dynamic_value(increment)) { - OV_CPU_JIT_EMITTER_ASSERT(use_runtime_args, "Loop argument structure cannot be pushed to aux GPR"); - h->add(Reg64(static_cast(data_ptr_reg_idxs[idx])), h->ptr[reg_increments + idx * sizeof(int64_t)]); - } else { - h->add(Reg64(static_cast(data_ptr_reg_idxs[idx])), increment * scale * data_sizes[idx]); + Reg64 reg_increments; + auto add_increments = [&]() { + for (size_t idx = 0; idx < data_ptr_reg_idxs.size(); idx++) { + const auto& increment = increments[idx]; + if (is_incremented[idx] && increment != 0) { + if (ov::snippets::utils::is_dynamic_value(increment)) { + OV_CPU_JIT_EMITTER_ASSERT(use_runtime_args, "Loop argument structure cannot be pushed to aux GPR"); + h->add(Reg64(static_cast(data_ptr_reg_idxs[idx])), h->ptr[reg_increments + idx * sizeof(int64_t)]); + } else { + h->add(Reg64(static_cast(data_ptr_reg_idxs[idx])), increment * scale * data_sizes[idx]); + } } } + }; + + const auto id_offset = loop_id * sizeof(jit_snippets_call_args::loop_args_t); + if (use_runtime_args) { + jit_aux_gpr_holder gpr_holder(h, aux_gpr_idxs, in); // loop_end has only input registers + reg_increments = gpr_holder.get_reg(); + h->mov(reg_increments, h->ptr[abi_param1 + GET_OFF(loop_args)]); + h->mov(reg_increments, h->ptr[reg_increments + id_offset + field_offset]); + add_increments(); + } else { + add_increments(); } }; diff --git a/src/plugins/intel_cpu/src/emitters/snippets/x64/jit_loop_emitters.hpp b/src/plugins/intel_cpu/src/emitters/snippets/x64/jit_loop_emitters.hpp index d7444dfd55cebc..262bba39b7d74c 100644 --- a/src/plugins/intel_cpu/src/emitters/snippets/x64/jit_loop_emitters.hpp +++ b/src/plugins/intel_cpu/src/emitters/snippets/x64/jit_loop_emitters.hpp @@ -31,7 +31,8 @@ class jit_loop_begin_emitter: public jit_emitter { void validate_arguments(const std::vector &in, const std::vector &out) const override; void emit_impl(const std::vector& in, const std::vector& out) const override; - size_t aux_gprs_count() const override; + // `jit_loop_begin_emitter` handles manually aux_gpr allocation using `jit_aux_gpr_holder` + size_t aux_gprs_count() const override { return 0; } std::shared_ptr loop_begin_label = nullptr; std::shared_ptr loop_end_label = nullptr; @@ -61,7 +62,8 @@ class jit_loop_end_emitter: public jit_emitter { void validate_arguments(const std::vector &in, const std::vector &out) const override; void emit_impl(const std::vector& in, const std::vector& out) const override; - size_t aux_gprs_count() const override; + // `jit_loop_end_emitter` handles manually aux_gpr allocation using `jit_aux_gpr_holder` + size_t aux_gprs_count() const override { return 0; } static ov::snippets::lowered::ExpressionPtr get_loop_begin_expr(const ov::snippets::lowered::ExpressionPtr& expr); diff --git a/src/plugins/intel_cpu/tests/functional/shared_tests_instances/snippets/mha.cpp b/src/plugins/intel_cpu/tests/functional/shared_tests_instances/snippets/mha.cpp index 19f1b230cd2c58..62edcba0de74e3 100644 --- a/src/plugins/intel_cpu/tests/functional/shared_tests_instances/snippets/mha.cpp +++ b/src/plugins/intel_cpu/tests/functional/shared_tests_instances/snippets/mha.cpp @@ -527,6 +527,35 @@ INSTANTIATE_TEST_SUITE_P( ::testing::Values(CPUTestUtils::empty_plugin_config)), MHA::getTestCaseName); +std::vector> inputShapes_4D_WithMul_dynamic{ + { + {PartialShape{-1, -1, -1, -1}, {{1, 128, 3, 64}, {1, 70, 3, 19}, {1, 128, 3, 64}, {1, 68, 6, 87}}}, + {PartialShape{-1, -1, -1, -1}, {{1, 128, 1, 64}, {2, 49, 1, 19}, {1, 128, 1, 64}, {2, 13, 6, 87}}}, + {PartialShape{1}, {{1}, {1}, {1}, {1} }}, + {PartialShape{-1, -1, -1, -1}, {{2, 1, 128, 128}, {1, 1, 70, 49}, {2, 1, 128, 128}, {1, 1, 68, 13}}}, + {PartialShape{-1, -1, -1, -1}, {{1, 128, 3, 64}, {1, 49, 3, 19}, {1, 128, 3, 64}, {2, 13, 6, 87}}}, + }, + { + {PartialShape{-1, -1, 12, 64}, {{1, 70, 12, 64}, {1, 20, 12, 64}, {1, 20, 12, 64}, {1, 20, 12, 64}, {1, 70, 12, 64}}}, + {PartialShape{-1, -1, 12, 64}, {{1, 35, 12, 64}, {2, 10, 12, 64}, {2, 1, 12, 64}, {2, 10, 12, 64}, {1, 35, 12, 64}}}, + {PartialShape{-1, 12, 64, -1}, {{1, 12, 64, 35}, {1, 12, 64, 10}, {1, 12, 64, 10}, {1, 12, 64, 1}, {1, 12, 64, 35}}}, + {PartialShape{-1, 12, -1, -1}, {{2, 12, 70, 35}, {1, 12, 20, 10}, {1, 12, 20, 10}, {1, 12, 20, 1}, {2, 12, 70, 35}}}, + {PartialShape{-1, -1, 12, 64}, {{1, 35, 12, 64}, {1, 10, 12, 64}, {1, 10, 12, 64}, {1, 10, 12, 64}, {1, 35, 12, 64}}}, + } +}; + +INSTANTIATE_TEST_SUITE_P(smoke_Snippets_DynMHA_4D_WithMul, + MHAWithDynamicMul, + ::testing::Combine(::testing::ValuesIn(inputShapes_4D_WithMul_dynamic), + ::testing::ValuesIn(precision_f32(5)), + ::testing::Values(ov::element::f32), + ::testing::Values(MHA::default_thread_count), + ::testing::Values(1), + ::testing::Values(1), + ::testing::Values(ov::test::utils::DEVICE_CPU), + ::testing::Values(CPUTestUtils::empty_plugin_config)), + MHAWithDynamicMul::getTestCaseName); + } // namespace } // namespace snippets } // namespace test diff --git a/src/tests/functional/plugin/shared/include/snippets/mha.hpp b/src/tests/functional/plugin/shared/include/snippets/mha.hpp index f73dba5d4ad5ce..0a1733a6099242 100644 --- a/src/tests/functional/plugin/shared/include/snippets/mha.hpp +++ b/src/tests/functional/plugin/shared/include/snippets/mha.hpp @@ -11,7 +11,7 @@ namespace ov { namespace test { namespace snippets { -typedef std::tuple, // Input shapes +typedef std::tuple, // Input shapes std::vector, // Input Element types ov::element::Type, // Inference precision bool, // With Multiply @@ -23,72 +23,101 @@ typedef std::tuple, // Input shapes > MHAParams; -class MHA : public testing::WithParamInterface, - virtual public ov::test::SnippetsTestsCommon { -public: - static std::string getTestCaseName(testing::TestParamInfo obj); +typedef std::tuple, // Input shapes + std::vector, // Input Element types + ov::element::Type, // Inference precision + size_t, // Thread count + size_t, // Expected num nodes + size_t, // Expected num subgraphs + std::string, // Target Device + ov::AnyMap // Config + > +MHAWithDynamicMulParams; +class MHABase : virtual public ov::test::SnippetsTestsCommon { +public: constexpr static size_t default_thread_count = 0; protected: void SetUp() override; - void compile_model() override; void generate_inputs(const std::vector& targetInputStaticShapes) override; - virtual std::shared_ptr get_subgraph(); + virtual std::shared_ptr get_subgraph() const = 0; + virtual void init_params(std::vector& input_shapes, ov::element::Type& prc, ov::AnyMap& additional_config) = 0; - bool m_with_mul = false; size_t m_thread_count; std::vector m_input_types; }; +class MHA : public testing::WithParamInterface, + virtual public MHABase { +public: + static std::string getTestCaseName(testing::TestParamInfo obj); + +protected: + std::shared_ptr get_subgraph() const override; + void init_params(std::vector& input_shapes, ov::element::Type& prc, ov::AnyMap& additional_config) override; + + bool m_with_mul = false; +}; + class MHASelect : public MHA { protected: void generate_inputs(const std::vector& targetInputStaticShapes) override; - std::shared_ptr get_subgraph() override; + std::shared_ptr get_subgraph() const override; }; class MHAWOTransposeOnInputs : public MHA { protected: - std::shared_ptr get_subgraph() override; + std::shared_ptr get_subgraph() const override; }; class MHAWOTranspose : public MHA { protected: - std::shared_ptr get_subgraph() override; + std::shared_ptr get_subgraph() const override; }; class MHAMulAdd : public MHA { - std::shared_ptr get_subgraph() override; + std::shared_ptr get_subgraph() const override; }; class MHATransposedB : public MHA { - std::shared_ptr get_subgraph() override; + std::shared_ptr get_subgraph() const override; }; class MHAINT8MatMul : public MHA { protected: - std::shared_ptr get_subgraph() override; + std::shared_ptr get_subgraph() const override; }; class MHAQuantMatMul0 : public MHA { protected: - std::shared_ptr get_subgraph() override; + std::shared_ptr get_subgraph() const override; }; class MHAFQAfterMatMul : public MHA { protected: - std::shared_ptr get_subgraph() override; + std::shared_ptr get_subgraph() const override; }; class MHAFQ : public MHA { protected: - std::shared_ptr get_subgraph() override; + std::shared_ptr get_subgraph() const override; }; class MHAWithExtractedReshape : public MHA { protected: - std::shared_ptr get_subgraph() override; + std::shared_ptr get_subgraph() const override; +}; + +class MHAWithDynamicMul : public testing::WithParamInterface, + virtual public MHABase { +public: + static std::string getTestCaseName(testing::TestParamInfo obj); + +protected: + std::shared_ptr get_subgraph() const override; + void init_params(std::vector& input_shapes, ov::element::Type& prc, ov::AnyMap& additional_config) override; }; } // namespace snippets diff --git a/src/tests/functional/plugin/shared/src/snippets/mha.cpp b/src/tests/functional/plugin/shared/src/snippets/mha.cpp index 9b5cbe2bafaf43..956b115213e3f1 100644 --- a/src/tests/functional/plugin/shared/src/snippets/mha.cpp +++ b/src/tests/functional/plugin/shared/src/snippets/mha.cpp @@ -14,6 +14,51 @@ namespace ov { namespace test { namespace snippets { +void MHABase::compile_model() { + if (m_thread_count != default_thread_count) + core->set_property(targetDevice, ov::inference_num_threads(m_thread_count)); + SubgraphBaseTest::compile_model(); +} + +void MHABase::generate_inputs(const std::vector& targetInputStaticShapes) { + inputs.clear(); + const auto& model_inputs = function->inputs(); + + for (int i = 0; i < model_inputs.size(); ++i) { + const auto& model_input = model_inputs[i]; + ov::Tensor tensor; + ov::test::utils::InputGenerateData in_data; + // To avoid big relative errors in the vicinity of zero, only positive values are generated for bf16 precision + in_data.start_from = model_input.get_element_type() == ov::element::bf16 ? 0 : -1; + in_data.range = 2; + in_data.resolution = 256; + tensor = + ov::test::utils::create_and_fill_tensor(model_input.get_element_type(), targetInputStaticShapes[i], in_data); + inputs.insert({model_input.get_node_shared_ptr(), tensor}); + } +} + +void MHABase::SetUp() { + std::vector input_shapes; + ov::element::Type prc; + ov::AnyMap additional_config; + init_params(input_shapes, prc, additional_config); + init_input_shapes(input_shapes); + + const auto subgraph_model = get_subgraph(); + function = subgraph_model->getOriginal(); + + configuration.insert(additional_config.begin(), additional_config.end()); + if (!configuration.count("SNIPPETS_MODE")) { + configuration.insert({"SNIPPETS_MODE", "IGNORE_CALLBACK"}); + } + + setInferenceType(prc); + inType = outType = prc; + if (prc == ov::element::bf16) + rel_threshold = 0.05f; +} + std::string MHA::getTestCaseName(testing::TestParamInfo obj) { std::vector input_shapes; std::vector elem_types; @@ -54,60 +99,46 @@ std::string MHA::getTestCaseName(testing::TestParamInfo obj) { std::vector input_shapes; + std::vector elem_types; ov::element::Type prc; + size_t thread_count; + std::string target_device; + size_t num_nodes, num_subgraphs; ov::AnyMap additional_config; - std::tie(input_shapes, - m_input_types, - prc, - m_with_mul, - m_thread_count, - ref_num_nodes, - ref_num_subgraphs, - targetDevice, - additional_config) = this->GetParam(); - init_input_shapes(input_shapes); + std::tie(input_shapes, elem_types, prc, thread_count, num_nodes, num_subgraphs, target_device, additional_config) = obj.param; - const auto subgraph_model = get_subgraph(); - function = subgraph_model->getOriginal(); + std::ostringstream result; + for (size_t i = 0; i < input_shapes.size(); i++) + result << "IS[" << i << "]=" << input_shapes[i] << "_"; + for (size_t i = 0; i < elem_types.size(); i++) + result << "T[" << i << "]=" << elem_types[i] << "_"; + result << "ThreadNum=" << thread_count << "_"; + result << "PRC=" << prc << "_"; + result << "#N=" << num_nodes << "_"; + result << "#S=" << num_subgraphs << "_"; + result << "targetDevice=" << target_device << "_"; - configuration.insert(additional_config.begin(), additional_config.end()); - if (!configuration.count("SNIPPETS_MODE")) { - configuration.insert({"SNIPPETS_MODE", "IGNORE_CALLBACK"}); + if (!additional_config.empty()) { + result << "_PluginConf"; + for (auto& item : additional_config) { + result << "_" << item.first << "=" << item.second.as(); + } } - - setInferenceType(prc); - inType = outType = prc; - if (prc == ov::element::bf16) - rel_threshold = 0.05f; + return result.str(); } -void MHA::compile_model() { - if (m_thread_count != default_thread_count) - core->set_property(targetDevice, ov::inference_num_threads(m_thread_count)); - SubgraphBaseTest::compile_model(); +void MHA::init_params(std::vector& input_shapes, ov::element::Type& prc, ov::AnyMap& additional_config) { + std::tie(input_shapes, m_input_types, prc, m_with_mul, m_thread_count, ref_num_nodes, ref_num_subgraphs, + targetDevice, additional_config) = this->GetParam(); } -void MHA::generate_inputs(const std::vector& targetInputStaticShapes) { - inputs.clear(); - const auto& model_inputs = function->inputs(); - - for (int i = 0; i < model_inputs.size(); ++i) { - const auto& model_input = model_inputs[i]; - ov::Tensor tensor; - ov::test::utils::InputGenerateData in_data; - // To avoid big relative errors in the vicinity of zero, only positive values are generated for bf16 precision - in_data.start_from = model_input.get_element_type() == ov::element::bf16 ? 0 : -1; - in_data.range = 2; - in_data.resolution = 256; - tensor = - ov::test::utils::create_and_fill_tensor(model_input.get_element_type(), targetInputStaticShapes[i], in_data); - inputs.insert({model_input.get_node_shared_ptr(), tensor}); - } +void MHAWithDynamicMul::init_params(std::vector& input_shapes, ov::element::Type& prc, ov::AnyMap& additional_config) { + std::tie(input_shapes, m_input_types, prc, m_thread_count, ref_num_nodes, ref_num_subgraphs, targetDevice, additional_config) = this->GetParam(); } -std::shared_ptr MHA::get_subgraph() { +std::shared_ptr MHA::get_subgraph() const { bool is_with_reshape = std::all_of(inputDynamicShapes.begin(), inputDynamicShapes.end(), [](const PartialShape& ps){ return ps.is_static(); }); return std::make_shared(inputDynamicShapes, m_input_types, m_with_mul, is_with_reshape); } @@ -143,46 +174,50 @@ void MHASelect::generate_inputs(const std::vector& targetInputStaticS } } -std::shared_ptr MHASelect::get_subgraph() { +std::shared_ptr MHASelect::get_subgraph() const { return std::make_shared(inputDynamicShapes, m_input_types); } -std::shared_ptr MHAWOTransposeOnInputs::get_subgraph() { +std::shared_ptr MHAWOTransposeOnInputs::get_subgraph() const { return std::make_shared(inputDynamicShapes); } -std::shared_ptr MHAWOTranspose::get_subgraph() { +std::shared_ptr MHAWOTranspose::get_subgraph() const { return std::make_shared(inputDynamicShapes, m_input_types); } -std::shared_ptr MHAINT8MatMul::get_subgraph() { +std::shared_ptr MHAINT8MatMul::get_subgraph() const { return std::make_shared(inputDynamicShapes); } -std::shared_ptr MHAQuantMatMul0::get_subgraph() { +std::shared_ptr MHAQuantMatMul0::get_subgraph() const { return std::make_shared(inputDynamicShapes); } -std::shared_ptr MHAFQAfterMatMul::get_subgraph() { +std::shared_ptr MHAFQAfterMatMul::get_subgraph() const { return std::make_shared(inputDynamicShapes); } -std::shared_ptr MHAFQ::get_subgraph() { +std::shared_ptr MHAFQ::get_subgraph() const { return std::make_shared(inputDynamicShapes); } -std::shared_ptr MHAMulAdd::get_subgraph() { +std::shared_ptr MHAMulAdd::get_subgraph() const { return std::make_shared(inputDynamicShapes); } -std::shared_ptr MHATransposedB::get_subgraph() { +std::shared_ptr MHATransposedB::get_subgraph() const { return std::make_shared(inputDynamicShapes, true); } -std::shared_ptr MHAWithExtractedReshape::get_subgraph() { +std::shared_ptr MHAWithExtractedReshape::get_subgraph() const { return std::make_shared(inputDynamicShapes, false); } +std::shared_ptr MHAWithDynamicMul::get_subgraph() const { + return std::make_shared(inputDynamicShapes, m_input_types); +} + TEST_P(MHA, CompareWithRefImpl) { SKIP_IF_CURRENT_TEST_IS_DISABLED() run(); @@ -250,6 +285,12 @@ TEST_P(MHAWithExtractedReshape, CompareWithRefImpl) { validateNumSubgraphs(); } +TEST_P(MHAWithDynamicMul, CompareWithRefImpl) { + SKIP_IF_CURRENT_TEST_IS_DISABLED() + run(); + validateNumSubgraphs(); +} + } // namespace snippets } // namespace test } // namespace ov diff --git a/src/tests/ov_helpers/ov_snippets_models/include/subgraph_mha.hpp b/src/tests/ov_helpers/ov_snippets_models/include/subgraph_mha.hpp index 60af12e27e5f48..90ab47214effee 100644 --- a/src/tests/ov_helpers/ov_snippets_models/include/subgraph_mha.hpp +++ b/src/tests/ov_helpers/ov_snippets_models/include/subgraph_mha.hpp @@ -70,6 +70,32 @@ class MHASplitMFunction : public MHAFunction { std::vector reshapes; }; +/* Graph: + * Transpose1[0,2,3,1] Parameter + * \ / + * Transpose0[0,2,1,3] Multiply + * \ / + * MatMul0 + * \ / + * Add + * Softmax Transpose2[0,2,1,3] + * \ / + * MatMul1 + * Transpose3[0,2,1,3] + */ +class MHAWithDynamicMulFunction : public SnippetsFunctionBase { +public: + explicit MHAWithDynamicMulFunction(const std::vector& inputShapes, const std::vector& precisions) + : SnippetsFunctionBase(inputShapes), precisions(precisions) { + OPENVINO_ASSERT(input_shapes.size() == 5, "Got invalid number of input shapes"); + OPENVINO_ASSERT(precisions.size() == 5, "Got invalid number of input precisions"); + } +protected: + std::shared_ptr initOriginal() const override; + + const std::vector precisions; +}; + /* Graph: * Transpose1[0,2,1,3] Constant * \ / diff --git a/src/tests/ov_helpers/ov_snippets_models/src/subgraph_mha.cpp b/src/tests/ov_helpers/ov_snippets_models/src/subgraph_mha.cpp index 3ca78918f5e925..c47cb754b5b891 100644 --- a/src/tests/ov_helpers/ov_snippets_models/src/subgraph_mha.cpp +++ b/src/tests/ov_helpers/ov_snippets_models/src/subgraph_mha.cpp @@ -231,6 +231,37 @@ std::shared_ptr MHASplitMFunction::initReference() const { return std::make_shared(results, ngraphParams, "mha"); } +std::shared_ptr MHAWithDynamicMulFunction::initOriginal() const { + auto transpose0Param = std::make_shared(precisions[0], input_shapes[0]); + auto transpose1Param = std::make_shared(precisions[1], input_shapes[1]); + auto mulParam = std::make_shared(precisions[2], input_shapes[2]); + auto addParam = std::make_shared(precisions[3], input_shapes[3]); + auto transpose2Param = std::make_shared(precisions[4], input_shapes[4]); + ov::ParameterVector ngraphParam = {transpose0Param, transpose1Param, mulParam, addParam, transpose2Param}; + + const auto rank = input_shapes[0].size(); + const auto fusion_order = get_fusion_order(rank); + const auto decomposed_order = get_decomposed_order(rank); + + const auto transpose0Const = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{rank}, fusion_order); + const auto transpose1Const = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{rank}, decomposed_order); + const auto transpose2Const = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{rank}, fusion_order); + const auto transpose3Const = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{rank}, fusion_order); + + const auto transpose0 = std::make_shared(transpose0Param, transpose0Const); + const auto transpose1 = std::make_shared(transpose1Param, transpose1Const); + const auto mul = std::make_shared(transpose1, mulParam); + const auto matMul0 = std::make_shared(transpose0, mul); + const auto add = std::make_shared(matMul0, addParam); + const auto softMax = std::make_shared(add, rank - 1); + const auto transpose2 = std::make_shared(transpose2Param, transpose2Const); + const auto matMul1 = std::make_shared(softMax, transpose2); + const auto transpose3 = std::make_shared(matMul1, transpose3Const); + + ov::ResultVector results{std::make_shared(transpose3)}; + return std::make_shared(results, ngraphParam, "mha"); +} + std::shared_ptr MHAMatMul0TransposeFunction::initOriginal() const { auto transpose0Param = std::make_shared(precisions[0], input_shapes[0]); auto transpose1Param = std::make_shared(precisions[1], input_shapes[1]); From bd7dc80242ee349da4c327ccaf7809932e3cff58 Mon Sep 17 00:00:00 2001 From: Vladimir Paramuzov Date: Thu, 22 Aug 2024 15:52:25 +0400 Subject: [PATCH 19/35] [GPU] Fix move per channel eltwise pass (#26154) ### Details: - Fixes regression after #24401 ### Tickets: - *CVS-150278* - *CVS-149636* - *CVS-149620* --- .../move_eltwise_up_data_movement.cpp | 3 ++- .../move_eltwise_up_data_movement_test.cpp | 20 +++++++++++++++++++ .../src/plugin/transformations_pipeline.cpp | 6 +++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/common/transformations/src/transformations/common_optimizations/move_eltwise_up_data_movement.cpp b/src/common/transformations/src/transformations/common_optimizations/move_eltwise_up_data_movement.cpp index fd9e7417261b81..82efb3699b2c40 100644 --- a/src/common/transformations/src/transformations/common_optimizations/move_eltwise_up_data_movement.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/move_eltwise_up_data_movement.cpp @@ -167,7 +167,8 @@ ov::pass::MoveEltwiseUpThroughDataMovPerChannel::MoveEltwiseUpThroughDataMovPerC }; auto eltw_data_flow_in = - ov::pass::pattern::wrap_type(); + ov::pass::pattern::wrap_type( + pattern::consumers_count(1)); auto eltw_const_in = ov::pass::pattern::wrap_type(const_predicate); auto eltwise_pattern = ov::pass::pattern::wrap_type({eltw_data_flow_in, eltw_const_in}, diff --git a/src/common/transformations/tests/common_optimizations/move_eltwise_up_data_movement_test.cpp b/src/common/transformations/tests/common_optimizations/move_eltwise_up_data_movement_test.cpp index 374cd208fefb7d..07eb3ec2b3e983 100644 --- a/src/common/transformations/tests/common_optimizations/move_eltwise_up_data_movement_test.cpp +++ b/src/common/transformations/tests/common_optimizations/move_eltwise_up_data_movement_test.cpp @@ -446,6 +446,26 @@ TEST_F(MoveEltwiseUpThroughDataMovTest, PerChannelEltwiseSqueezeIllegal_1) { manager.register_pass(); } +TEST_F(MoveEltwiseUpThroughDataMovTest, PerChannelEltwiseSqueezeIllegal_2) { + const ov::Shape shape{10, 20, 1, 1}; + // Data movement op with multiple consumers is not applicable + auto input = std::make_shared(ov::element::f32, shape); + + auto squeeze_const = ov::opset8::Constant::create(ov::element::i64, ov::Shape{}, {2}); // {10, 20, 1} + auto squeeze = std::make_shared(input, squeeze_const); + + auto per_channel_const1 = ov::opset8::Constant::create(ov::element::f32, {10, 1, 1}, {0.5}); + auto add1 = std::make_shared(squeeze, per_channel_const1); + + auto per_channel_const2 = ov::opset8::Constant::create(ov::element::f32, {10, 1, 1}, {0.5}); + auto add2 = std::make_shared(squeeze, per_channel_const2); + + auto add3 = std::make_shared(add1, add2); + + model = std::make_shared(ov::NodeVector{add3}, ov::ParameterVector{input}); + manager.register_pass(); +} + TEST_F(MoveEltwiseUpThroughDataMovTest, PerChannelReshapeMultiply) { const ov::Shape shape{1, 3, 20}; const std::vector target_shape = {1, 3, 4, 5}; diff --git a/src/plugins/intel_gpu/src/plugin/transformations_pipeline.cpp b/src/plugins/intel_gpu/src/plugin/transformations_pipeline.cpp index f5cb6783d4b080..fc62f68af2f6a2 100644 --- a/src/plugins/intel_gpu/src/plugin/transformations_pipeline.cpp +++ b/src/plugins/intel_gpu/src/plugin/transformations_pipeline.cpp @@ -794,7 +794,7 @@ void TransformationsPipeline::apply(std::shared_ptr func) { ov::op::v1::Broadcast::get_type_info_static(), ov::op::v3::Broadcast::get_type_info_static(), }; - manager.register_pass(allowed_data_movement_ops); + manager.register_pass(allowed_data_movement_ops); manager.register_pass(); manager.register_pass(); @@ -842,6 +842,10 @@ void TransformationsPipeline::apply(std::shared_ptr func) { manager.register_pass(); manager.register_pass(); manager.register_pass(); + + // This pass should be done after asymmetric quantization matching as it can move zp subtraction upper in the graph + manager.register_pass(); + manager.register_pass(); const size_t zp_pad_size = device_info.supports_immad ? 16 : 32; From 3c34cc6520bd386ee572a417fa779bcd416efa3c Mon Sep 17 00:00:00 2001 From: Damian Kurek Date: Thu, 22 Aug 2024 14:46:48 +0200 Subject: [PATCH 20/35] [GPU] Improve maximum value calculation in softmax BF kernel (#26101) ### Details: - Calculate maximum values for each work-item while fetching data, slightly improving performance - Remove one loop - Decrease private memory fetch ### Tickets: - 150093 --- .../cl_kernels/softmax_gpu_bf.cl | 48 +++++++------------ 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/softmax_gpu_bf.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/softmax_gpu_bf.cl index c391afdc4adbb4..990da1509558bd 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/softmax_gpu_bf.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/softmax_gpu_bf.cl @@ -87,6 +87,7 @@ KERNEL (softmax_gpu_continuous_bfyx)( INPUT0_TYPE my_chunk[STACK_SIZE]; INPUT0_TYPE my_sum = UNIT_VAL_ZERO; + INPUT0_TYPE my_maximum = -UNIT_VAL_MAX; __local INPUT0_TYPE lg_storage[SLM_SIZE]; @@ -108,6 +109,7 @@ KERNEL (softmax_gpu_continuous_bfyx)( unroll_for (int j = 0; j < OPT_BLOCK_SIZE; j++) { my_chunk[input_idx+j] = vec_tmp[j]; + my_maximum = max(my_maximum, vec_tmp[j]); } } @@ -115,6 +117,7 @@ KERNEL (softmax_gpu_continuous_bfyx)( { BLOCK_TYPE vec_tmp = BLOCK_READ(input, aligned_data_offset + input_idx * get_sub_group_size()); my_chunk[input_idx] = vec_tmp; + my_maximum = max(my_maximum, vec_tmp); } } #else @@ -125,11 +128,13 @@ KERNEL (softmax_gpu_continuous_bfyx)( BLOCK_TYPE vec_tmp = BLOCK_READ(input, aligned_data_offset + input_idx * get_sub_group_size()); #if SUBGROUP_BLOCK_SIZE == 1 my_chunk[input_idx] = vec_tmp; + my_maximum = max(my_maximum, vec_tmp); #else unroll_for (int j = 0; j < SUBGROUP_BLOCK_SIZE; j++) { INPUT0_TYPE tmp = vec_tmp[j]; my_chunk[input_idx+j] = tmp; + my_maximum = max(my_maximum, tmp); } #endif } @@ -138,29 +143,23 @@ KERNEL (softmax_gpu_continuous_bfyx)( for (; input_idx < items_num; input_idx++) { - my_chunk[input_idx] = input[aligned_data_offset + get_sub_group_local_id() + input_idx * get_sub_group_size()]; + INPUT0_TYPE tmp = input[aligned_data_offset + get_sub_group_local_id() + input_idx * get_sub_group_size()]; + my_chunk[input_idx] = tmp; + my_maximum = max(my_maximum, tmp); } if (in_data_set_idx < aligned_offset) { INPUT0_TYPE tmp = input[data_set_offset + in_data_set_idx]; my_chunk[input_idx++] = tmp; + my_maximum = max(my_maximum, tmp); } if (in_data_set_idx < actual_leftovers) { INPUT0_TYPE tmp = input[leftover_idx]; my_chunk[input_idx++] = tmp; - } - - INPUT0_TYPE my_maximum = -UNIT_VAL_MAX; - { - const uint num_iters = input_idx; - - for (uint j=0; j Date: Thu, 22 Aug 2024 15:52:16 +0200 Subject: [PATCH 21/35] Use AvgPool-14 in PT FE (#25471) ### Details: - Extend PT FE with AvgPool-14 - Move the `count_include_pad` workaround subgraph from PT FE to a downgrade transformation - The subgraph in downgrade transformations is defined in https://github.com/openvinotoolkit/openvino/blob/master/src/common/transformations/src/transformations/op_conversions/convert_avgpool_downgrade.cpp#L52 - The new `RoundingType::CEIL_TORCH` has impact only on the output shape - Out of two tests which previously have been xfailed due to output shape mismatch one is still failing, but now due to accuracy validation failure - No old tests were broken by upgrading to V14, so it seems the subgraph has issue with this edge case specifically - The edge case is connected to `count_include_pad:True`, and `pad` value, depending on the latter FP16 test case sometimes passes, but FP32 always fails - Since CPU Plugin is calling oneDNN AvgPool implementation there may be some difference in results calculation or an issue with parameter preprocessing before calling oneDNN function - Continued in XXXXX ### Tickets: - CVS-133929 --------- Co-authored-by: Michal Lukaszewski --- src/frontends/pytorch/src/op/avg_poolnd.cpp | 24 ++----------------- .../layer_tests/pytorch_tests/test_pooling.py | 18 +++++--------- 2 files changed, 8 insertions(+), 34 deletions(-) diff --git a/src/frontends/pytorch/src/op/avg_poolnd.cpp b/src/frontends/pytorch/src/op/avg_poolnd.cpp index 65e1e3ba92dc97..03c32259b45091 100644 --- a/src/frontends/pytorch/src/op/avg_poolnd.cpp +++ b/src/frontends/pytorch/src/op/avg_poolnd.cpp @@ -40,35 +40,15 @@ OutputVector translate_avg_poolnd(const NodeContext& context) { } ov::op::RoundingType rounding_type = ov::op::RoundingType::FLOOR; if (!(context.input_is_none(4))) { - rounding_type = context.const_input(4) ? ov::op::RoundingType::CEIL : ov::op::RoundingType::FLOOR; + rounding_type = context.const_input(4) ? ov::op::RoundingType::CEIL_TORCH : ov::op::RoundingType::FLOOR; } if (!(context.input_is_none(5))) { count_include_pad = context.const_input(5); } PYTORCH_OP_CONVERSION_CHECK(context.input_is_none(6), "Translation for aten::avg_pool2d do not support divisor_override input."); - // Although ov::AvgPool provides exclude_pad=false, - // The corner case of Average Pooling with ceil_mode on - // PyTorch allows sliding window go off bound, which leads to this accommodation. - // More detail on https://github.com/pytorch/pytorch/issues/57178 - if (count_include_pad) { - auto zero = context.mark_node(v0::Constant::create(element::f32, Shape{}, {0})); - zero = context.mark_node(std::make_shared(zero, input)); - auto zero_i32 = context.mark_node(v0::Constant::create(element::i32, Shape{}, {0})); - Output rank; - std::tie(std::ignore, rank) = get_shape_rank(context, input); - auto pad_values = get_input_as_i32(context, 3); - auto pads_len = context.mark_node(v0::Constant::create(element::i32, Shape{}, {pads.size()})); - auto pads_diff = context.mark_node(std::make_shared(rank, pads_len)); - auto pads_remaining = context.mark_node(std::make_shared(zero_i32, pads_diff)); - auto padding = context.mark_node( - std::make_shared(OutputVector{std::move(pads_remaining), std::move(pad_values)}, 0)); - input = context.mark_node(std::make_shared(input, padding, padding, zero, ov::op::PadMode::CONSTANT)); - pads = Shape(pads.size(), 0); - } - return {context.mark_node( - std::make_shared(input, strides, pads, pads, kernel, !count_include_pad, rounding_type))}; + std::make_shared(input, strides, pads, pads, kernel, !count_include_pad, rounding_type))}; }; } // namespace op diff --git a/tests/layer_tests/pytorch_tests/test_pooling.py b/tests/layer_tests/pytorch_tests/test_pooling.py index 0cf2446abd1125..32c8a973cb1c92 100644 --- a/tests/layer_tests/pytorch_tests/test_pooling.py +++ b/tests/layer_tests/pytorch_tests/test_pooling.py @@ -17,10 +17,9 @@ {'kernel_size': [2, 1], 'stride': [2, 1], 'padding': 0}, {'kernel_size': [2, 1], 'stride': None, 'padding': 0}, {'kernel_size': [2, 1], 'stride': [], 'padding': 0}, + {'kernel_size': [8, 8], 'stride': [8, 4], 'padding': 1}, ] -d2_params_corner_case = [{'kernel_size': [8, 8], 'stride': [8, 4], 'padding': 1}] - d1_params = [{'kernel_size': 3, 'stride': 1, 'padding': 0}, {'kernel_size': (4,), 'stride': 1, 'padding': 1}, {'kernel_size': 4, 'stride': (5,), 'padding': 2}, @@ -144,14 +143,7 @@ def test_avg_pool1d(self, params, ceil_mode, count_include_pad, ie_device, preci dynamic_shapes=False) @pytest.mark.parametrize( - "params", - d2_params - + [ - pytest.param( - {"kernel_size": [8, 8], "stride": [8, 4], "padding": 1}, - marks=pytest.mark.xfail(reason="Sliding windows that would start in the right padded are ignored.") - ) - ]) + "params", d2_params) @pytest.mark.parametrize("ceil_mode", [True, False]) @pytest.mark.parametrize("count_include_pad", [True, False]) @pytest.mark.nightly @@ -161,6 +153,8 @@ def test_avg_pool1d(self, params, ceil_mode, count_include_pad, ie_device, preci @pytest.mark.xfail(condition=platform.system() == 'Darwin' and platform.machine() == 'arm64', reason='Ticket - 122715') def test_avg_pool2d(self, params, ceil_mode, count_include_pad, ie_device, precision, ir_version): + if ceil_mode and count_include_pad and np.array_equal(np.array(params["kernel_size"]), np.array([8, 8])): + pytest.xfail("Ticket - 150292") self._test(*self.create_model("avg_pool2d", **params, ceil_mode=ceil_mode, count_include_pad=count_include_pad), ie_device, precision, ir_version, trace_model=True, freeze_model=False, dynamic_shapes=False) @@ -190,7 +184,7 @@ def test_max_pool1d(self, params, ceil_mode, dilation, ie_device, precision, ir_ self._test(*self.create_model("max_pool1d", **params, ceil_mode=ceil_mode, dilation=dilation), ie_device, precision, ir_version, kwargs_to_prepare_input={'ndim': 3}, dynamic_shapes=False) - @pytest.mark.parametrize("params", d2_params + d2_params_corner_case) + @pytest.mark.parametrize("params", d2_params) @pytest.mark.parametrize("ceil_mode", [True, False]) @pytest.mark.parametrize("dilation", [1, 2]) @pytest.mark.parametrize("dtype", [torch.float32, torch.int32]) @@ -227,7 +221,7 @@ def test_max_pool1d_indices(self, params, ceil_mode, dilation, ie_device, precis self._test(*self.create_model("max_pool1d_with_indices", **params, ceil_mode=ceil_mode, dilation=dilation), ie_device, precision, ir_version, kwargs_to_prepare_input={'ndim': 3}, dynamic_shapes=False) - @pytest.mark.parametrize("params", d2_params + d2_params_corner_case) + @pytest.mark.parametrize("params", d2_params) @pytest.mark.parametrize("ceil_mode", [True, False]) @pytest.mark.parametrize("dilation", [1, 2]) @pytest.mark.nightly From f50a8764d5b4804ab1f126e6be11328c53ed7ca0 Mon Sep 17 00:00:00 2001 From: Tatiana Savina Date: Thu, 22 Aug 2024 16:31:40 +0200 Subject: [PATCH 22/35] [DOCS] Bump zipp in docs (#26179) ### Details: recreates PR 25474 ### Tickets: - *ticket-id* --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 5703503a9ba158..a3873d9e77c27e 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -46,4 +46,4 @@ sphinxcontrib-qthelp==1.0.3 sphinxcontrib-serializinghtml==1.1.9 toml==0.10.2 urllib3==1.26.19 -zipp==3.4.1 \ No newline at end of file +zipp==3.19.1 \ No newline at end of file From 407b012ce353b991a04a094e8de691fc477fd96d Mon Sep 17 00:00:00 2001 From: Vladimir Paramuzov Date: Thu, 22 Aug 2024 18:40:38 +0400 Subject: [PATCH 23/35] [GPU] SYCL infra for GPU plugin and GHA pipeline (#26067) Co-authored-by: Ilya Lavrenov --- .github/dockerfiles/docker_tag | 2 +- .../ubuntu_22_04_x64_dpcpp/Dockerfile | 95 +++++++++++++++++++ .github/workflows/ubuntu_22_dpcpp.yml | 90 ++++++++++++++++++ .../compile_flags/os_flags.cmake | 2 + .../scaled_attn/executor_pa_common.hpp | 1 + src/plugins/intel_gpu/CMakeLists.txt | 4 + .../intel_gpu/graph/kernel_impl_params.hpp | 2 +- .../primitives/implementation_desc.hpp | 1 + .../runtime/engine_configuration.hpp | 2 + .../intel_gpu/src/graph/CMakeLists.txt | 10 ++ .../intel_gpu/src/graph/impls/sycl/README.md | 16 ++++ .../graph/impls/sycl/primitive_sycl_base.h | 90 ++++++++++++++++++ .../src/graph/impls/sycl/register.cpp | 17 ++++ .../src/graph/impls/sycl/register.hpp | 23 +++++ src/plugins/intel_gpu/src/graph/program.cpp | 6 ++ src/plugins/intel_gpu/src/plugin/plugin.cpp | 4 + .../intel_gpu/src/plugin/remote_context.cpp | 5 + .../intel_gpu/src/runtime/CMakeLists.txt | 11 +++ .../intel_gpu/src/runtime/device_query.cpp | 1 + src/plugins/intel_gpu/src/runtime/engine.cpp | 5 + .../intel_gpu/src/runtime/kernels_cache.cpp | 4 +- .../intel_gpu/src/runtime/kernels_factory.cpp | 4 +- src/plugins/intel_gpu/src/runtime/memory.cpp | 4 +- .../src/runtime/ocl/ocl_engine_factory.hpp | 3 + .../intel_gpu/src/runtime/ocl/sycl_engine.cpp | 37 ++++++++ .../intel_gpu/src/runtime/ocl/sycl_engine.hpp | 28 ++++++ .../intel_gpu/src/runtime/ocl/sycl_stream.cpp | 30 ++++++ .../intel_gpu/src/runtime/ocl/sycl_stream.hpp | 26 +++++ src/plugins/intel_gpu/src/runtime/stream.cpp | 4 +- .../intel_gpu/tests/unit/CMakeLists.txt | 5 + .../intel_npu/src/plugin/CMakeLists.txt | 2 +- 31 files changed, 526 insertions(+), 8 deletions(-) create mode 100644 .github/dockerfiles/ov_build/ubuntu_22_04_x64_dpcpp/Dockerfile create mode 100644 .github/workflows/ubuntu_22_dpcpp.yml create mode 100644 src/plugins/intel_gpu/src/graph/impls/sycl/README.md create mode 100644 src/plugins/intel_gpu/src/graph/impls/sycl/primitive_sycl_base.h create mode 100644 src/plugins/intel_gpu/src/graph/impls/sycl/register.cpp create mode 100644 src/plugins/intel_gpu/src/graph/impls/sycl/register.hpp create mode 100644 src/plugins/intel_gpu/src/runtime/ocl/sycl_engine.cpp create mode 100644 src/plugins/intel_gpu/src/runtime/ocl/sycl_engine.hpp create mode 100644 src/plugins/intel_gpu/src/runtime/ocl/sycl_stream.cpp create mode 100644 src/plugins/intel_gpu/src/runtime/ocl/sycl_stream.hpp diff --git a/.github/dockerfiles/docker_tag b/.github/dockerfiles/docker_tag index 38479d3f7437dc..5bd51d5f04378a 100644 --- a/.github/dockerfiles/docker_tag +++ b/.github/dockerfiles/docker_tag @@ -1 +1 @@ -pr-25992 \ No newline at end of file +pr-26067 diff --git a/.github/dockerfiles/ov_build/ubuntu_22_04_x64_dpcpp/Dockerfile b/.github/dockerfiles/ov_build/ubuntu_22_04_x64_dpcpp/Dockerfile new file mode 100644 index 00000000000000..292ae46cab94a3 --- /dev/null +++ b/.github/dockerfiles/ov_build/ubuntu_22_04_x64_dpcpp/Dockerfile @@ -0,0 +1,95 @@ +FROM openvinogithubactions.azurecr.io/dockerhub/ubuntu:22.04 + +USER root + +# APT configuration +RUN echo 'Acquire::Retries "10";' > /etc/apt/apt.conf && \ + echo 'APT::Get::Assume-Yes "true";' >> /etc/apt/apt.conf && \ + echo 'APT::Get::Fix-Broken "true";' >> /etc/apt/apt.conf && \ + echo 'APT::Get::no-install-recommends "true";' >> /etc/apt/apt.conf + +ENV DEBIAN_FRONTEND="noninteractive" \ + TZ="Europe/London" + +RUN apt-get update && \ + apt-get install software-properties-common && \ + add-apt-repository --yes --no-update ppa:git-core/ppa && \ + add-apt-repository --yes --no-update ppa:deadsnakes/ppa && \ + apt-get update && \ + apt-get install \ + wget \ + curl \ + git \ + ca-certificates \ + gpg-agent \ + tzdata \ + libtbb2 \ + # Pythons \ + python3.11-dev \ + python3.11-venv \ + python3.11-distutils \ + default-jdk \ + && \ + rm -rf /var/lib/apt/lists/* + + +# Install OneAPI Toolkit +RUN wget -O- https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB | gpg --dearmor | tee /usr/share/keyrings/intel-oneapi-archive-keyring.gpg > /dev/null +RUN echo "deb [signed-by=/usr/share/keyrings/intel-oneapi-archive-keyring.gpg] https://apt.repos.intel.com/oneapi all main " > /etc/apt/sources.list.d/oneAPI.list + +RUN apt-get update && \ + apt-get install \ + intel-oneapi-compiler-dpcpp-cpp=2024.2.1-1079 \ + && \ + rm -rf /var/lib/apt/lists/* + +# Install build dependencies +ADD install_build_dependencies.sh /install_build_dependencies.sh +RUN chmod +x /install_build_dependencies.sh && \ + /install_build_dependencies.sh && \ + rm -rf /var/lib/apt/lists/* + +# Install sscache +ARG SCCACHE_VERSION="v0.7.5" +ENV SCCACHE_HOME="/opt/sccache" \ + SCCACHE_PATH="/opt/sccache/sccache" + +RUN mkdir ${SCCACHE_HOME} && cd ${SCCACHE_HOME} && \ + SCCACHE_ARCHIVE="sccache-${SCCACHE_VERSION}-x86_64-unknown-linux-musl.tar.gz" && \ + curl -SLO https://github.com/mozilla/sccache/releases/download/${SCCACHE_VERSION}/${SCCACHE_ARCHIVE} && \ + tar -xzf ${SCCACHE_ARCHIVE} --strip-components=1 && rm ${SCCACHE_ARCHIVE} + +# Setup pip +ENV PIP_VERSION="24.0" +RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \ + python3 get-pip.py --no-cache-dir pip==${PIP_VERSION} && \ + python3.11 get-pip.py --no-cache-dir pip==${PIP_VERSION} && \ + rm -f get-pip.py + +# Use Python 3.11 as default +# Using venv here 'cause other methods to switch the default Python on Ubuntu break both system and wheels build +RUN python3.11 -m venv venv +ENV PATH="/venv/bin:$SCCACHE_HOME:$PATH" + +ENV PIP_CACHE_DIR=/mount/caches/pip/linux/${PIP_VERSION} + +# OneAPI env +ENV ONEAPI_ROOT=/opt/intel/oneapi +ENV PKG_CONFIG_PATH=/opt/intel/oneapi/compiler/2024.2/lib/pkgconfig +ENV DIAGUTIL_PATH=/opt/intel/oneapi/dpcpp-ct/2024.2/etc/dpct/sys_check/sys_check.sh:/opt/intel/oneapi/debugger/2024.2/etc/debugger/sys_check/sys_check.py:/opt/intel/oneapi/compiler/2024.2/etc/compiler/sys_check/sys_check.sh +ENV MANPATH=/opt/intel/oneapi/debugger/2024.2/share/man:/opt/intel/oneapi/compiler/2024.2/share/man: +ENV GDB_INFO=/opt/intel/oneapi/debugger/2024.2/share/info/ +ENV CMAKE_PREFIX_PATH=/opt/intel/oneapi/compiler/2024.2 +ENV CMPLR_ROOT=/opt/intel/oneapi/compiler/2024.2 +ENV INFOPATH=/opt/intel/oneapi/debugger/2024.2/share/info +ENV LIBRARY_PATH=/opt/intel/oneapi/compiler/2024.2/lib +ENV OCL_ICD_FILENAMES=/opt/intel/oneapi/compiler/2024.2/lib/libintelocl.so +ENV LD_LIBRARY_PATH=/opt/intel/oneapi/debugger/2024.2/opt/debugger/lib:/opt/intel/oneapi/compiler/2024.2/opt/compiler/lib:/opt/intel/oneapi/compiler/2024.2/lib +ENV NLSPATH=/opt/intel/oneapi/mkl/2024.2/share/locale/%l_%t/%N:/opt/intel/oneapi/compiler/2024.2/lib/compiler/locale/%l_%t/%N +ENV PATH=$PATH:/opt/intel/oneapi/dev-utilities/2024.2/bin:/opt/intel/oneapi/debugger/2024.2/opt/debugger/bin:/opt/intel/oneapi/compiler/2024.2/bin +ENV INTEL_PYTHONHOME=/opt/intel/oneapi/debugger/2024.2/opt/debugger +ENV CPATH=/opt/intel/oneapi/dpl/2022.6/include:/opt/intel/oneapi/dev-utilities/2024.2/include: + +# Set Intel DPC++ as a default compiler +ENV CC=icx +ENV CXX=icpx diff --git a/.github/workflows/ubuntu_22_dpcpp.yml b/.github/workflows/ubuntu_22_dpcpp.yml new file mode 100644 index 00000000000000..4af7b08a334996 --- /dev/null +++ b/.github/workflows/ubuntu_22_dpcpp.yml @@ -0,0 +1,90 @@ +name: Linux (Ubuntu 22.04, Python 3.11, Intel DPC++ Compiler) +on: + workflow_dispatch: + pull_request: + merge_group: + +concurrency: + # github.ref is not unique in post-commit + group: ${{ github.event_name == 'push' && github.run_id || github.ref }}-ubuntu-22-dpcpp + cancel-in-progress: true + +permissions: read-all + +jobs: + Smart_CI: + runs-on: ubuntu-latest + outputs: + affected_components: "${{ steps.smart_ci.outputs.affected_components }}" + changed_components: "${{ steps.smart_ci.outputs.changed_components }}" + skip_workflow: "${{ steps.smart_ci.outputs.skip_workflow }}" + steps: + - name: checkout action + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + with: + sparse-checkout: .github/actions/smart-ci + + - name: Get affected components + id: smart_ci + uses: ./.github/actions/smart-ci + with: + repository: ${{ github.repository }} + pr: ${{ github.event.number }} + commit_sha: ${{ github.sha }} + ref_name: ${{ github.ref_name }} + component_pattern: "category: (.*)" + repo_token: ${{ secrets.GITHUB_TOKEN }} + skip_when_only_listed_labels_set: 'docs' + skip_when_only_listed_files_changed: '*.md,*.rst,*.png,*.jpg,*.svg' + + - name: Show affected components + run: | + echo "${{ toJSON(steps.smart_ci.outputs.affected_components) }}" + shell: bash + + Docker: + needs: Smart_CI + runs-on: aks-linux-4-cores-16gb-docker-build + container: + image: openvinogithubactions.azurecr.io/docker_build:0.2 + volumes: + - /mount:/mount + outputs: + images: "${{ steps.handle_docker.outputs.images }}" + steps: + - name: Checkout + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - uses: ./.github/actions/handle_docker + id: handle_docker + with: + images: | + ov_build/ubuntu_22_04_x64_dpcpp + registry: 'openvinogithubactions.azurecr.io' + dockerfiles_root_dir: '.github/dockerfiles' + changed_components: ${{ needs.smart_ci.outputs.changed_components }} + + Build: + needs: [Docker, Smart_CI] + if: "!needs.smart_ci.outputs.skip_workflow" + uses: ./.github/workflows/job_build_linux.yml + with: + runner: 'aks-linux-16-cores-32gb' + container: '{"image": "${{ fromJSON(needs.docker.outputs.images).ov_build.ubuntu_22_04_x64_dpcpp }}", "volumes": ["/mount:/mount"], "options": "-e SCCACHE_AZURE_BLOB_CONTAINER -e SCCACHE_AZURE_CONNECTION_STRING"}' + affected-components: ${{ needs.smart_ci.outputs.affected_components }} + event-name: ${{ github.event_name }} + os: 'ubuntu_22_04_dpcpp' + + Overall_Status: + name: ci/gha_overall_status_ubuntu_22.04_dpcpp + needs: [Smart_CI, Build] + if: ${{ always() }} + runs-on: ubuntu-latest + steps: + - name: Check status of all jobs + if: >- + ${{ + contains(needs.*.result, 'failure') || + contains(needs.*.result, 'cancelled') + }} + run: exit 1 diff --git a/cmake/developer_package/compile_flags/os_flags.cmake b/cmake/developer_package/compile_flags/os_flags.cmake index a49dce9bce7b50..bb80787ee3d811 100644 --- a/cmake/developer_package/compile_flags/os_flags.cmake +++ b/cmake/developer_package/compile_flags/os_flags.cmake @@ -289,6 +289,8 @@ endif() if(NOT DEFINED CMAKE_CXX_STANDARD) if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") set(CMAKE_CXX_STANDARD 14) + elseif(OV_COMPILER_IS_INTEL_LLVM) + set(CMAKE_CXX_STANDARD 17) else() set(CMAKE_CXX_STANDARD 11) endif() diff --git a/src/plugins/intel_cpu/src/nodes/kernels/scaled_attn/executor_pa_common.hpp b/src/plugins/intel_cpu/src/nodes/kernels/scaled_attn/executor_pa_common.hpp index 46e06466d5f8ba..237860ec692e76 100644 --- a/src/plugins/intel_cpu/src/nodes/kernels/scaled_attn/executor_pa_common.hpp +++ b/src/plugins/intel_cpu/src/nodes/kernels/scaled_attn/executor_pa_common.hpp @@ -34,6 +34,7 @@ struct PagedAttentionExecutor { static const size_t ID_ALIBI_SLOPES = 11; // [H|0], float static const size_t ID_MAX_CONTEXT_LEN = 12; // [] virtual void execute(const std::vector& inputs, const std::vector outputs) = 0; + virtual ~PagedAttentionExecutor() = default; }; #ifdef OPENVINO_ARCH_X86_64 diff --git a/src/plugins/intel_gpu/CMakeLists.txt b/src/plugins/intel_gpu/CMakeLists.txt index 53b4a203301c77..795d259b277e2b 100644 --- a/src/plugins/intel_gpu/CMakeLists.txt +++ b/src/plugins/intel_gpu/CMakeLists.txt @@ -8,6 +8,10 @@ endif() set (TARGET_NAME "openvino_intel_gpu_plugin") +if(OV_COMPILER_IS_INTEL_LLVM) + find_package(IntelSYCL REQUIRED) +endif() + if((CMAKE_COMPILER_IS_GNUCXX OR OV_COMPILER_IS_CLANG) AND CMAKE_CXX_STANDARD GREATER_EQUAL 20) set(CMAKE_CXX_FLAGS "-Wno-error=deprecated ${CMAKE_CXX_FLAGS}") endif() diff --git a/src/plugins/intel_gpu/include/intel_gpu/graph/kernel_impl_params.hpp b/src/plugins/intel_gpu/include/intel_gpu/graph/kernel_impl_params.hpp index fa8a8807bbd92c..3e8887fbb2f7ee 100644 --- a/src/plugins/intel_gpu/include/intel_gpu/graph/kernel_impl_params.hpp +++ b/src/plugins/intel_gpu/include/intel_gpu/graph/kernel_impl_params.hpp @@ -90,7 +90,7 @@ struct kernel_impl_params final { , primary_input_idx(0) { } - virtual ~kernel_impl_params() = default; + ~kernel_impl_params() = default; const layout& get_input_layout(size_t idx = 0) const { OPENVINO_ASSERT(input_layouts.size() > idx, diff --git a/src/plugins/intel_gpu/include/intel_gpu/primitives/implementation_desc.hpp b/src/plugins/intel_gpu/include/intel_gpu/primitives/implementation_desc.hpp index 4e5c53d6b37e3e..e84311a9cfb592 100644 --- a/src/plugins/intel_gpu/include/intel_gpu/primitives/implementation_desc.hpp +++ b/src/plugins/intel_gpu/include/intel_gpu/primitives/implementation_desc.hpp @@ -18,6 +18,7 @@ enum class impl_types : uint8_t { common = 1 << 1, ocl = 1 << 2, onednn = 1 << 3, + sycl = 1 << 4, any = 0xFF, }; diff --git a/src/plugins/intel_gpu/include/intel_gpu/runtime/engine_configuration.hpp b/src/plugins/intel_gpu/include/intel_gpu/runtime/engine_configuration.hpp index 042fa7cfd3dc67..e81dd9dec9d2f5 100644 --- a/src/plugins/intel_gpu/include/intel_gpu/runtime/engine_configuration.hpp +++ b/src/plugins/intel_gpu/include/intel_gpu/runtime/engine_configuration.hpp @@ -11,11 +11,13 @@ namespace cldnn { /// @brief Defines available engine types enum class engine_types : int32_t { ocl, + sycl }; inline std::ostream& operator<<(std::ostream& os, const engine_types& type) { switch (type) { case engine_types::ocl: os << "ocl"; break; + case engine_types::sycl: os << "sycl"; break; default: os << "unknown"; break; } diff --git a/src/plugins/intel_gpu/src/graph/CMakeLists.txt b/src/plugins/intel_gpu/src/graph/CMakeLists.txt index db25a90e42b446..d82984205c0307 100644 --- a/src/plugins/intel_gpu/src/graph/CMakeLists.txt +++ b/src/plugins/intel_gpu/src/graph/CMakeLists.txt @@ -4,6 +4,7 @@ set(TARGET_NAME "openvino_intel_gpu_graph") +file(GLOB_RECURSE SYCL_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/impls/sycl/*") file(GLOB_RECURSE LIBRARY_SRC "${INCLUDE_DIR}/*.h" "${INCLUDE_DIR}/*.hpp" @@ -22,6 +23,10 @@ if (NOT ENABLE_ONEDNN_FOR_GPU) endforeach(SOURCE_FILE) endif() +if(NOT OV_COMPILER_IS_INTEL_LLVM) + list(REMOVE_ITEM LIBRARY_SRC ${SYCL_SOURCES}) +endif() + add_library(${TARGET_NAME} STATIC ${LIBRARY_SRC}) target_include_directories(${TARGET_NAME} PUBLIC @@ -33,6 +38,11 @@ target_include_directories(${TARGET_NAME} PUBLIC target_compile_options(${TARGET_NAME} PRIVATE $<$:$,/Os,-Os>>) +if(OV_COMPILER_IS_INTEL_LLVM) + add_sycl_to_target(TARGET ${TARGET_NAME} SOURCES ${SYCL_SOURCES}) + target_compile_definitions(${TARGET_NAME} PUBLIC OV_GPU_WITH_SYCL) +endif() + target_link_libraries(${TARGET_NAME} PUBLIC OpenCL::OpenCL openvino::shape_inference) target_link_libraries(${TARGET_NAME} PRIVATE openvino_intel_gpu_kernels openvino_intel_gpu_runtime diff --git a/src/plugins/intel_gpu/src/graph/impls/sycl/README.md b/src/plugins/intel_gpu/src/graph/impls/sycl/README.md new file mode 100644 index 00000000000000..851ef8f5d18519 --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/impls/sycl/README.md @@ -0,0 +1,16 @@ +# How to build with DPC++ support + +1. Install OneAPI base toolkit. Guide: https://www.intel.com/content/www/us/en/docs/oneapi/installation-guide-linux/2024-0/installation.html +2. Export environment: + $ source /opt/intel/oneapi/setvars.sh + +3. configure cmake with the following additional options: + - `-DCMAKE_CXX_FLAGS:STRING=--gcc-install-dir=/lib/gcc/x86_64-linux-gnu/12/ -DCMAKE_C_FLAGS:STRING=--gcc-install-dir=/lib/gcc/x86_64-linux-gnu/12/` + - This WA is needed if multiple GCC version available in the system + - `-DCMAKE_CXX_STANDARD:STRING=17` + - Sycl requires c++17 + - `-DENABLE_SYSTEM_OPENCL=OFF` + - May help to avoid opencl icd/header conflicts as sycl package may have no clhpp headers + - `-DCMAKE_C_COMPILER:FILEPATH=icx -DCMAKE_CXX_COMPILER:FILEPATH=icpx` + - For now find_package(IntelSYCL) doesn't work if compiler is not icpx +4. make -j$(nproc) diff --git a/src/plugins/intel_gpu/src/graph/impls/sycl/primitive_sycl_base.h b/src/plugins/intel_gpu/src/graph/impls/sycl/primitive_sycl_base.h new file mode 100644 index 00000000000000..a6b0e3512e80b3 --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/impls/sycl/primitive_sycl_base.h @@ -0,0 +1,90 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "primitive_inst.h" +#include "intel_gpu/runtime/memory.hpp" +#include "register.hpp" +#include "utils.hpp" +#include "runtime/ocl/ocl_event.hpp" + +#include + +#include "sycl/sycl.hpp" + +namespace cldnn { +namespace sycl { + +static std::mutex cacheAccessMutex; + +template +struct typed_primitive_sycl_impl : public typed_primitive_impl { + const engine* _engine; + + typed_primitive_sycl_impl(const engine& engine, const ExecutionConfig& config, std::shared_ptr weights_reorder = nullptr) + : typed_primitive_impl(weights_reorder, "sycl_kernel"), + _engine(&engine) { } + + typed_primitive_sycl_impl() + : typed_primitive_impl({}, "undef"), + _engine(nullptr) { + } + + bool is_cpu() const override { return false; } + bool is_onednn() const override { return false; } + + void save(BinaryOutputBuffer& ob) const override { + } + + void load(BinaryInputBuffer& ib) override { + } + +protected: + void init_kernels(const kernels_cache&, const kernel_impl_params&) override { } + + void set_arguments_impl(typed_primitive_inst& instance) override { + if (instance.can_be_optimized()) + return; + } + + void update_dispatch_data(const kernel_impl_params& impl_params) override {} + + void set_arguments_impl(typed_primitive_inst& instance, kernel_arguments_data& args) override { + if (instance.can_be_optimized()) { + return; + } + } + + event::ptr execute_impl(const std::vector& /* events */, + typed_primitive_inst& instance) override { + auto& network = instance.get_network(); + auto& stream = network.get_stream(); + auto net_id = network.get_id(); + event::ptr event; + + + return event; + } + + static event::ptr to_ocl_event(stream& stream, ::sycl::event e) { + if (stream.get_queue_type() == QueueTypes::out_of_order) { + auto native_events = get_native<::sycl::backend::opencl, ::sycl::event>(e); + std::vector events; + for (auto& e : native_events) { + events.push_back(std::make_shared(cl::Event(e, true))); + } + return events.empty() ? stream.create_user_event(true) : stream.group_events(events); + } else { + return stream.create_user_event(true); + } + } + + std::vector get_internal_buffer_layouts_impl() const override { + return {}; + } +}; + +} // namespace sycl +} // namespace cldnn diff --git a/src/plugins/intel_gpu/src/graph/impls/sycl/register.cpp b/src/plugins/intel_gpu/src/graph/impls/sycl/register.cpp new file mode 100644 index 00000000000000..9d2ae6808fbfc6 --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/impls/sycl/register.cpp @@ -0,0 +1,17 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "register.hpp" + +namespace cldnn { +namespace sycl { + +#define REGISTER_SYCL_IMPL(prim) \ + static detail::attach_##prim##_sycl attach_##prim + +void register_implementations() { +} + +} // namespace sycl +} // namespace cldnn diff --git a/src/plugins/intel_gpu/src/graph/impls/sycl/register.hpp b/src/plugins/intel_gpu/src/graph/impls/sycl/register.hpp new file mode 100644 index 00000000000000..38fa9df02c5d88 --- /dev/null +++ b/src/plugins/intel_gpu/src/graph/impls/sycl/register.hpp @@ -0,0 +1,23 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + + +namespace cldnn { +namespace sycl { +void register_implementations(); + +namespace detail { + +#define REGISTER_SYCL_IMPL(prim) \ + struct attach_##prim##_sycl { \ + attach_##prim##_sycl(); \ + } + +#undef REGISTER_SYCL_IMPL + +} // namespace detail +} // namespace sycl +} // namespace cldnn diff --git a/src/plugins/intel_gpu/src/graph/program.cpp b/src/plugins/intel_gpu/src/graph/program.cpp index 820bb87f4a9d6f..40d858c38fe85a 100644 --- a/src/plugins/intel_gpu/src/graph/program.cpp +++ b/src/plugins/intel_gpu/src/graph/program.cpp @@ -75,6 +75,9 @@ #ifdef ENABLE_ONEDNN_FOR_GPU #include "impls/onednn/register.hpp" #endif +#ifdef OV_GPU_WITH_SYCL +#include "impls/sycl/register.hpp" +#endif #include "kernel_base.h" @@ -257,6 +260,9 @@ void program::init_primitives() { onednn::register_implementations(); #endif cpu::register_implementations(); +#ifdef OV_GPU_WITH_SYCL + sycl::register_implementations(); +#endif is_initialized = true; } } diff --git a/src/plugins/intel_gpu/src/plugin/plugin.cpp b/src/plugins/intel_gpu/src/plugin/plugin.cpp index e3f7c2cfe2f6b5..69bc3a912fda43 100644 --- a/src/plugins/intel_gpu/src/plugin/plugin.cpp +++ b/src/plugins/intel_gpu/src/plugin/plugin.cpp @@ -145,7 +145,11 @@ Plugin::Plugin() { register_primitives(); // Set OCL runtime which should be always available +#ifdef OV_GPU_WITH_SYCL + cldnn::device_query device_query(cldnn::engine_types::sycl, cldnn::runtime_types::ocl); +#else cldnn::device_query device_query(cldnn::engine_types::ocl, cldnn::runtime_types::ocl); +#endif m_device_map = device_query.get_available_devices(); // Set default configs for each device diff --git a/src/plugins/intel_gpu/src/plugin/remote_context.cpp b/src/plugins/intel_gpu/src/plugin/remote_context.cpp index b36fe7f38a776e..6d24eaea401689 100644 --- a/src/plugins/intel_gpu/src/plugin/remote_context.cpp +++ b/src/plugins/intel_gpu/src/plugin/remote_context.cpp @@ -28,7 +28,12 @@ Type extract_object(const ov::AnyMap& params, const ov::Property& p) { RemoteContextImpl::RemoteContextImpl(const std::string& device_name, std::vector devices) : m_device_name(device_name) { OPENVINO_ASSERT(devices.size() == 1, "[GPU] Currently context can be created for single device only"); +#ifdef OV_GPU_WITH_SYCL + const auto engine_type = cldnn::engine_types::sycl; +#else const auto engine_type = cldnn::engine_types::ocl; +#endif + const auto runtime_type = cldnn::runtime_types::ocl; m_engine = cldnn::engine::create(engine_type, runtime_type, devices.front()); diff --git a/src/plugins/intel_gpu/src/runtime/CMakeLists.txt b/src/plugins/intel_gpu/src/runtime/CMakeLists.txt index 1439620b71e386..23fce3a3354756 100644 --- a/src/plugins/intel_gpu/src/runtime/CMakeLists.txt +++ b/src/plugins/intel_gpu/src/runtime/CMakeLists.txt @@ -21,6 +21,12 @@ set(LIBRARY_SOURCES_ALL ${LIBRARY_SOURCES_OCL} ) +file(GLOB_RECURSE SYCL_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/ocl/sycl_*.cpp") + +if(NOT OV_COMPILER_IS_INTEL_LLVM) + list(REMOVE_ITEM LIBRARY_SOURCES_ALL ${SYCL_SOURCES}) +endif() + add_library(${TARGET_NAME} STATIC ${LIBRARY_SOURCES_ALL}) if(NOT BUILD_SHARED_LIBS) @@ -44,6 +50,11 @@ target_compile_options(${TARGET_NAME} PRIVATE add_cpplint_target(${TARGET_NAME}_cpplint FOR_TARGETS ${TARGET_NAME}) +if(OV_COMPILER_IS_INTEL_LLVM) + add_sycl_to_target(TARGET ${TARGET_NAME} SOURCES ${SYCL_SOURCES}) + target_compile_definitions(${TARGET_NAME} PUBLIC OV_GPU_WITH_SYYCL) +endif() + ov_set_threading_interface_for(${TARGET_NAME}) target_link_libraries(${TARGET_NAME} PRIVATE diff --git a/src/plugins/intel_gpu/src/runtime/device_query.cpp b/src/plugins/intel_gpu/src/runtime/device_query.cpp index 7cb419deee4f19..4306e7aada874b 100644 --- a/src/plugins/intel_gpu/src/runtime/device_query.cpp +++ b/src/plugins/intel_gpu/src/runtime/device_query.cpp @@ -17,6 +17,7 @@ device_query::device_query(engine_types engine_type, int ctx_device_id, int target_tile_id) { switch (engine_type) { + case engine_types::sycl: case engine_types::ocl: { if (runtime_type != runtime_types::ocl) throw std::runtime_error("Unsupported runtime type for ocl engine"); diff --git a/src/plugins/intel_gpu/src/runtime/engine.cpp b/src/plugins/intel_gpu/src/runtime/engine.cpp index e8879905ffe40a..7d1d016b77b5ff 100644 --- a/src/plugins/intel_gpu/src/runtime/engine.cpp +++ b/src/plugins/intel_gpu/src/runtime/engine.cpp @@ -252,6 +252,11 @@ void engine::subtract_memory_used(uint64_t bytes, allocation_type type) { std::shared_ptr engine::create(engine_types engine_type, runtime_types runtime_type, const device::ptr device) { std::shared_ptr ret; switch (engine_type) { +#ifdef OV_GPU_WITH_SYCL + case engine_types::sycl: + ret = ocl::create_sycl_engine(device, runtime_type); + break; +#endif // OV_GPU_WITH_SYCL case engine_types::ocl: ret = ocl::create_ocl_engine(device, runtime_type); break; diff --git a/src/plugins/intel_gpu/src/runtime/kernels_cache.cpp b/src/plugins/intel_gpu/src/runtime/kernels_cache.cpp index 3f19b348cca600..d6dcaf5dd72d14 100644 --- a/src/plugins/intel_gpu/src/runtime/kernels_cache.cpp +++ b/src/plugins/intel_gpu/src/runtime/kernels_cache.cpp @@ -620,7 +620,7 @@ void kernels_cache::add_to_cached_kernels(const std::vector& kernel } void kernels_cache::save(BinaryOutputBuffer& ob) const { - OPENVINO_ASSERT(_engine.type() == engine_types::ocl, "[GPU] Not supported engine type"); + OPENVINO_ASSERT(_engine.type() == engine_types::ocl || _engine.type() == engine_types::sycl, "[GPU] Not supported engine type"); ob << _cached_binaries.size(); for (auto& cached_binary : _cached_binaries) { @@ -630,7 +630,7 @@ void kernels_cache::save(BinaryOutputBuffer& ob) const { } void kernels_cache::load(BinaryInputBuffer& ib) { - OPENVINO_ASSERT(_engine.type() == engine_types::ocl, "[GPU] Not supported engine type"); + OPENVINO_ASSERT(_engine.type() == engine_types::ocl || _engine.type() == engine_types::sycl, "[GPU] Not supported engine type"); std::unordered_map> precompiled_kernels; diff --git a/src/plugins/intel_gpu/src/runtime/kernels_factory.cpp b/src/plugins/intel_gpu/src/runtime/kernels_factory.cpp index 7dd7be87365204..448afa863609f3 100644 --- a/src/plugins/intel_gpu/src/runtime/kernels_factory.cpp +++ b/src/plugins/intel_gpu/src/runtime/kernels_factory.cpp @@ -10,7 +10,9 @@ namespace kernels_factory { std::shared_ptr create(engine& engine, cl_context context, cl_kernel kernel, std::string entry_point) { switch (engine.type()) { - case engine_types::ocl: return ocl::create_ocl_kernel(engine, context, kernel, entry_point); + case engine_types::sycl: + case engine_types::ocl: + return ocl::create_ocl_kernel(engine, context, kernel, entry_point); default: throw std::runtime_error("Unsupported engine type in kernels_factory::create"); } } diff --git a/src/plugins/intel_gpu/src/runtime/memory.cpp b/src/plugins/intel_gpu/src/runtime/memory.cpp index 4e7f9b37c393f7..5e79af34c6ce14 100644 --- a/src/plugins/intel_gpu/src/runtime/memory.cpp +++ b/src/plugins/intel_gpu/src/runtime/memory.cpp @@ -47,7 +47,9 @@ memory::memory(engine* engine, const layout& layout, allocation_type type, std:: std::unique_ptr surfaces_lock::create(engine_types engine_type, std::vector mem, const stream& stream) { switch (engine_type) { - case engine_types::ocl: return std::unique_ptr(new ocl::ocl_surfaces_lock(mem, stream)); + case engine_types::sycl: + case engine_types::ocl: + return std::unique_ptr(new ocl::ocl_surfaces_lock(mem, stream)); default: throw std::runtime_error("Unsupported engine type in surfaces_lock::create"); } } diff --git a/src/plugins/intel_gpu/src/runtime/ocl/ocl_engine_factory.hpp b/src/plugins/intel_gpu/src/runtime/ocl/ocl_engine_factory.hpp index fad0a483e0c988..2ddfe28ceeb214 100644 --- a/src/plugins/intel_gpu/src/runtime/ocl/ocl_engine_factory.hpp +++ b/src/plugins/intel_gpu/src/runtime/ocl/ocl_engine_factory.hpp @@ -14,6 +14,9 @@ namespace ocl { // Factory for ocl_engine creation. It's moved outside of ocl_engine class to avoid possible CL includes conflict // between different engines in engine.cpp file std::shared_ptr create_ocl_engine(const device::ptr device, runtime_types runtime_type); +#ifdef OV_GPU_WITH_SYCL +std::shared_ptr create_sycl_engine(const device::ptr device, runtime_types runtime_type); +#endif // OV_GPU_WITH_SYCL } // namespace ocl } // namespace cldnn diff --git a/src/plugins/intel_gpu/src/runtime/ocl/sycl_engine.cpp b/src/plugins/intel_gpu/src/runtime/ocl/sycl_engine.cpp new file mode 100644 index 00000000000000..b5da32efbfa8c0 --- /dev/null +++ b/src/plugins/intel_gpu/src/runtime/ocl/sycl_engine.cpp @@ -0,0 +1,37 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "sycl_engine.hpp" +#include "sycl_stream.hpp" + + +namespace cldnn { +namespace ocl { + + +sycl_engine::sycl_engine(const device::ptr dev, runtime_types runtime_type) + : ocl_engine(dev, runtime_type) { + sycl_context = cldnn::make_unique<::sycl::context>(sycl::make_context<::sycl::backend::opencl>(get_cl_context().get())); +} + +stream::ptr sycl_engine::create_stream(const ExecutionConfig& config) const { + return std::make_shared(*this, config); +} + +stream::ptr sycl_engine::create_stream(const ExecutionConfig& config, void* handle) const { + return std::make_shared(*this, config, handle); +} + +std::shared_ptr create_sycl_engine(const device::ptr device, runtime_types runtime_type) { + return std::make_shared(device, runtime_type); +} + +::sycl::context& sycl_engine::get_sycl_context() const { + OPENVINO_ASSERT(sycl_context != nullptr); + + return *sycl_context; +} + +} // namespace ocl +} // namespace cldnn diff --git a/src/plugins/intel_gpu/src/runtime/ocl/sycl_engine.hpp b/src/plugins/intel_gpu/src/runtime/ocl/sycl_engine.hpp new file mode 100644 index 00000000000000..253f6af6795db9 --- /dev/null +++ b/src/plugins/intel_gpu/src/runtime/ocl/sycl_engine.hpp @@ -0,0 +1,28 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include "ocl_engine.hpp" + +#include "sycl/sycl.hpp" + + +namespace cldnn { +namespace ocl { + +class sycl_engine : public ocl_engine { +public: + sycl_engine(const device::ptr dev, runtime_types runtime_type); + + stream_ptr create_stream(const ExecutionConfig& config) const override; + stream_ptr create_stream(const ExecutionConfig& config, void *handle) const override; + + ::sycl::context& get_sycl_context() const; + std::unique_ptr<::sycl::context> sycl_context = nullptr; +}; + +} // namespace ocl +} // namespace cldnn diff --git a/src/plugins/intel_gpu/src/runtime/ocl/sycl_stream.cpp b/src/plugins/intel_gpu/src/runtime/ocl/sycl_stream.cpp new file mode 100644 index 00000000000000..e9be7a060ba2e4 --- /dev/null +++ b/src/plugins/intel_gpu/src/runtime/ocl/sycl_stream.cpp @@ -0,0 +1,30 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "sycl_stream.hpp" +#include "intel_gpu/runtime/utils.hpp" +#include "sycl_engine.hpp" + +namespace cldnn { +namespace ocl { + + +sycl_stream::sycl_stream(const sycl_engine& engine, const ExecutionConfig& config) + : ocl_stream(engine, config) { + sycl_queue = cldnn::make_unique<::sycl::queue>(::sycl::make_queue<::sycl::backend::opencl>(get_cl_queue().get(), engine.get_sycl_context())); +} + + +sycl_stream::sycl_stream(const sycl_engine &engine, const ExecutionConfig& config, void *handle) + : ocl_stream(engine, config, handle) { + sycl_queue = cldnn::make_unique<::sycl::queue>(::sycl::make_queue<::sycl::backend::opencl>(get_cl_queue().get(), engine.get_sycl_context())); +} + +::sycl::queue& sycl_stream::get_sycl_queue() { + OPENVINO_ASSERT(sycl_queue != nullptr); + return *sycl_queue; +} + +} // namespace ocl +} // namespace cldnn diff --git a/src/plugins/intel_gpu/src/runtime/ocl/sycl_stream.hpp b/src/plugins/intel_gpu/src/runtime/ocl/sycl_stream.hpp new file mode 100644 index 00000000000000..208b155d519dba --- /dev/null +++ b/src/plugins/intel_gpu/src/runtime/ocl/sycl_stream.hpp @@ -0,0 +1,26 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "ocl_stream.hpp" +#include "sycl_engine.hpp" + +#include "sycl/sycl.hpp" + + +namespace cldnn { +namespace ocl { + +class sycl_stream : public ocl_stream { +public: + sycl_stream(const sycl_engine& engine, const ExecutionConfig& config); + sycl_stream(const sycl_engine &engine, const ExecutionConfig& config, void *handle); + + ::sycl::queue& get_sycl_queue(); + std::unique_ptr<::sycl::queue> sycl_queue = nullptr; +}; + +} // namespace ocl +} // namespace cldnn diff --git a/src/plugins/intel_gpu/src/runtime/stream.cpp b/src/plugins/intel_gpu/src/runtime/stream.cpp index 0ee63a78b806e5..0fab4d38a1402a 100644 --- a/src/plugins/intel_gpu/src/runtime/stream.cpp +++ b/src/plugins/intel_gpu/src/runtime/stream.cpp @@ -12,7 +12,9 @@ namespace cldnn { QueueTypes stream::detect_queue_type(engine_types engine_type, void* queue_handle) { switch (engine_type) { - case engine_types::ocl: return ocl::ocl_stream::detect_queue_type(queue_handle); + case engine_types::sycl: + case engine_types::ocl: + return ocl::ocl_stream::detect_queue_type(queue_handle); default: throw std::runtime_error("Invalid engine type"); } } diff --git a/src/plugins/intel_gpu/tests/unit/CMakeLists.txt b/src/plugins/intel_gpu/tests/unit/CMakeLists.txt index 8bff8f56c50156..2c7a4b12735cc0 100644 --- a/src/plugins/intel_gpu/tests/unit/CMakeLists.txt +++ b/src/plugins/intel_gpu/tests/unit/CMakeLists.txt @@ -11,6 +11,11 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") ov_add_compiler_flags(/wd4305) endif() +if(OV_COMPILER_IS_INTEL_LLVM) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=lld") + string(REPLACE "-pie" "" CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +endif() + set(TARGET_NAME "ov_gpu_unit_tests") file(GLOB_RECURSE SOURCES_MAIN diff --git a/src/plugins/intel_npu/src/plugin/CMakeLists.txt b/src/plugins/intel_npu/src/plugin/CMakeLists.txt index f071d001d44994..30e4eae254de90 100644 --- a/src/plugins/intel_npu/src/plugin/CMakeLists.txt +++ b/src/plugins/intel_npu/src/plugin/CMakeLists.txt @@ -66,7 +66,7 @@ target_include_directories(${TARGET_NAME} ${NPU_PLUGIN_SOURCE_DIR}/thirdparty/level-zero-ext ) -if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU") +if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU" OR OV_COMPILER_IS_INTEL_LLVM) target_compile_options(${TARGET_NAME} PRIVATE -mavx2 -mf16c) elseif(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") target_compile_options(${TARGET_NAME} PRIVATE /arch:AVX2) From b38eb133e9fa0676441e807291a04ed243d639be Mon Sep 17 00:00:00 2001 From: Ilya Lavrenov Date: Fri, 23 Aug 2024 01:45:48 +0400 Subject: [PATCH 24/35] Cmake: generalized THREADING=OMP to work on supported platforms (#26175) ### Details: - *item1* - *...* ### Tickets: - *ticket-id* --- cmake/dependencies.cmake | 94 +++++----- .../developer_package/packaging/archive.cmake | 2 + .../packaging/common-libraries.cmake | 2 + .../packaging/debian/debian.cmake | 2 + cmake/developer_package/packaging/npm.cmake | 2 + cmake/developer_package/packaging/nsis.cmake | 2 + .../developer_package/packaging/rpm/rpm.cmake | 2 + cmake/features.cmake | 14 +- cmake/templates/OpenVINOConfig.cmake.in | 54 +++++- .../OpenVINODeveloperPackageConfig.cmake.in | 3 +- ...DeveloperPackageConfigRelocatable.cmake.in | 4 +- src/bindings/python/CMakeLists.txt | 4 +- src/bindings/python/wheel/CMakeLists.txt | 5 - src/bindings/python/wheel/setup.py | 8 + src/cmake/ov_parallel.cmake | 168 ++++++++++-------- src/common/util/src/file_util.cpp | 4 +- 16 files changed, 230 insertions(+), 140 deletions(-) diff --git a/cmake/dependencies.cmake b/cmake/dependencies.cmake index 2ebfceb8b9612e..a152ff85c9c768 100644 --- a/cmake/dependencies.cmake +++ b/cmake/dependencies.cmake @@ -9,55 +9,55 @@ ov_set_temp_directory(TEMP "${CMAKE_SOURCE_DIR}") ## Intel OMP package if(THREADING STREQUAL "OMP") - reset_deps_cache(OMP) - if(WIN32 AND X86_64) - RESOLVE_DEPENDENCY(OMP - ARCHIVE_WIN "iomp.zip" - TARGET_PATH "${TEMP}/omp" - ENVIRONMENT "OMP" - VERSION_REGEX ".*_([a-z]*_([a-z0-9]+\\.)*[0-9]+).*" - SHA256 "62c68646747fb10f19b53217cb04a1e10ff93606f992e6b35eb8c31187c68fbf" - USE_NEW_LOCATION TRUE) - elseif(LINUX AND X86_64 AND OPENVINO_GNU_LIBC) - RESOLVE_DEPENDENCY(OMP - ARCHIVE_LIN "iomp.tgz" - TARGET_PATH "${TEMP}/omp" - ENVIRONMENT "OMP" - VERSION_REGEX ".*_([a-z]*_([a-z0-9]+\\.)*[0-9]+).*" - SHA256 "7832b16d82513ee880d97c27c7626f9525ebd678decf6a8fe6c38550f73227d9" - USE_NEW_LOCATION TRUE) - elseif(APPLE AND X86_64) - RESOLVE_DEPENDENCY(OMP - ARCHIVE_MAC "iomp_20190130_mac.tgz" - TARGET_PATH "${TEMP}/omp" - ENVIRONMENT "OMP" - VERSION_REGEX ".*_([a-z]*_([a-z0-9]+\\.)*[0-9]+).*" - SHA256 "591ea4a7e08bbe0062648916f42bded71d24c27f00af30a8f31a29b5878ea0cc" - USE_NEW_LOCATION TRUE) - else() - message(WARNING "Pre-built Intel OMP is not available on current platform. System OMP will be used.") - find_package(OpenMP) - if(OpenMP_CXX_FOUND) - foreach(OpenMP_LIB ${OpenMP_CXX_LIBRARIES}) - string(FIND ${OpenMP_LIB} "omp" OpenMP_LIB_OMP_INDEX) - if(NOT OpenMP_LIB_OMP_INDEX EQUAL -1) - cmake_path(GET OpenMP_LIB PARENT_PATH OpenMP_LIB_DIR) - set(OMP_LIB ${OpenMP_LIB} CACHE FILEPATH "Path to OMP library") - set(OMP ${OpenMP_LIB_DIR} CACHE FILEPATH "Path to OMP root folder") - return() - endif() - endforeach() + # check whether the compiler supports OpenMP at all + find_package(OpenMP) + + if(NOT OpenMP_CXX_FOUND) + message(WARNING "Compiler does not support OpenMP standard. Falling back to SEQ threading") + set(THREADING "SEQ") + set(ENABLE_INTEL_OPENMP OFF) + endif() + + if(ENABLE_INTEL_OPENMP) + reset_deps_cache(OMP) + if(WIN32 AND X86_64) + RESOLVE_DEPENDENCY(INTEL_OMP + ARCHIVE_WIN "iomp.zip" + TARGET_PATH "${TEMP}/omp" + ENVIRONMENT "INTEL_OMP" + VERSION_REGEX ".*_([a-z]*_([a-z0-9]+\\.)*[0-9]+).*" + SHA256 "62c68646747fb10f19b53217cb04a1e10ff93606f992e6b35eb8c31187c68fbf" + USE_NEW_LOCATION TRUE) + elseif(LINUX AND X86_64 AND OPENVINO_GNU_LIBC) + RESOLVE_DEPENDENCY(INTEL_OMP + ARCHIVE_LIN "iomp.tgz" + TARGET_PATH "${TEMP}/omp" + ENVIRONMENT "INTEL_OMP" + VERSION_REGEX ".*_([a-z]*_([a-z0-9]+\\.)*[0-9]+).*" + SHA256 "7832b16d82513ee880d97c27c7626f9525ebd678decf6a8fe6c38550f73227d9" + USE_NEW_LOCATION TRUE) + elseif(APPLE AND X86_64) + RESOLVE_DEPENDENCY(INTEL_OMP + ARCHIVE_MAC "iomp_20190130_mac.tgz" + TARGET_PATH "${TEMP}/omp" + ENVIRONMENT "INTEL_OMP" + VERSION_REGEX ".*_([a-z]*_([a-z0-9]+\\.)*[0-9]+).*" + SHA256 "591ea4a7e08bbe0062648916f42bded71d24c27f00af30a8f31a29b5878ea0cc" + USE_NEW_LOCATION TRUE) + endif() + + if(INTEL_OMP) + update_deps_cache(INTEL_OMP "${INTEL_OMP}" "Path to OMP root folder") + debug_message(STATUS "intel_omp=" ${INTEL_OMP}) + + ov_cpack_add_component(omp HIDDEN) + file(GLOB_RECURSE source_list "${INTEL_OMP}/*${CMAKE_SHARED_LIBRARY_SUFFIX}*") + install(FILES ${source_list} + DESTINATION ${OV_CPACK_RUNTIMEDIR} + COMPONENT intel_omp + ${OV_CPACK_COMP_OPENMP_EXCLUDE_ALL}) endif() - message(FATAL_ERROR "System OpenMP has not been found") endif() - update_deps_cache(OMP "${OMP}" "Path to OMP root folder") - debug_message(STATUS "intel_omp=" ${OMP}) - - ov_cpack_add_component(omp HIDDEN) - file(GLOB_RECURSE source_list "${OMP}/*${CMAKE_SHARED_LIBRARY_SUFFIX}*") - install(FILES ${source_list} - DESTINATION ${OV_CPACK_RUNTIMEDIR} - COMPONENT omp) endif() ## TBB package diff --git a/cmake/developer_package/packaging/archive.cmake b/cmake/developer_package/packaging/archive.cmake index da6c676de6c574..6df5145ae2e488 100644 --- a/cmake/developer_package/packaging/archive.cmake +++ b/cmake/developer_package/packaging/archive.cmake @@ -74,6 +74,8 @@ macro(ov_define_component_include_rules) # tbb unset(OV_CPACK_COMP_TBB_EXCLUDE_ALL) unset(OV_CPACK_COMP_TBB_DEV_EXCLUDE_ALL) + # openmp + unset(OV_CPACK_COMP_OPENMP_EXCLUDE_ALL) # licensing unset(OV_CPACK_COMP_LICENSING_EXCLUDE_ALL) # samples diff --git a/cmake/developer_package/packaging/common-libraries.cmake b/cmake/developer_package/packaging/common-libraries.cmake index 4ec96dc28b53e8..581c9144c1b907 100644 --- a/cmake/developer_package/packaging/common-libraries.cmake +++ b/cmake/developer_package/packaging/common-libraries.cmake @@ -79,6 +79,8 @@ macro(ov_define_component_include_rules) # tbb set(OV_CPACK_COMP_TBB_EXCLUDE_ALL EXCLUDE_FROM_ALL) set(OV_CPACK_COMP_TBB_DEV_EXCLUDE_ALL EXCLUDE_FROM_ALL) + # openmp + set(OV_CPACK_COMP_OPENMP_EXCLUDE_ALL EXCLUDE_FROM_ALL) # licensing if(CPACK_GENERATOR STREQUAL "CONAN") unset(OV_CPACK_COMP_LICENSING_EXCLUDE_ALL) diff --git a/cmake/developer_package/packaging/debian/debian.cmake b/cmake/developer_package/packaging/debian/debian.cmake index c7f49419111cea..f133428d66ec74 100644 --- a/cmake/developer_package/packaging/debian/debian.cmake +++ b/cmake/developer_package/packaging/debian/debian.cmake @@ -82,6 +82,8 @@ macro(ov_define_component_include_rules) # tbb set(OV_CPACK_COMP_TBB_EXCLUDE_ALL EXCLUDE_FROM_ALL) set(OV_CPACK_COMP_TBB_DEV_EXCLUDE_ALL EXCLUDE_FROM_ALL) + # openmp + set(OV_CPACK_COMP_OPENMP_EXCLUDE_ALL EXCLUDE_FROM_ALL) # licensing set(OV_CPACK_COMP_LICENSING_EXCLUDE_ALL EXCLUDE_FROM_ALL) # samples diff --git a/cmake/developer_package/packaging/npm.cmake b/cmake/developer_package/packaging/npm.cmake index 24453965125348..2a2509cdcae65a 100644 --- a/cmake/developer_package/packaging/npm.cmake +++ b/cmake/developer_package/packaging/npm.cmake @@ -61,6 +61,8 @@ macro(ov_define_component_include_rules) # tbb unset(OV_CPACK_COMP_TBB_EXCLUDE_ALL) set(OV_CPACK_COMP_TBB_DEV_EXCLUDE_ALL EXCLUDE_FROM_ALL) + # openmp + unset(OV_CPACK_COMP_OPENMP_EXCLUDE_ALL) # licensing unset(OV_CPACK_COMP_LICENSING_EXCLUDE_ALL) # samples diff --git a/cmake/developer_package/packaging/nsis.cmake b/cmake/developer_package/packaging/nsis.cmake index f5f9a233e8b87f..d7d8ed152c49d9 100644 --- a/cmake/developer_package/packaging/nsis.cmake +++ b/cmake/developer_package/packaging/nsis.cmake @@ -116,6 +116,8 @@ macro(ov_define_component_include_rules) # tbb unset(OV_CPACK_COMP_TBB_EXCLUDE_ALL) unset(OV_CPACK_COMP_TBB_DEV_EXCLUDE_ALL) + # openmp + unset(OV_CPACK_COMP_OPENMP_EXCLUDE_ALL) # licensing unset(OV_CPACK_COMP_LICENSING_EXCLUDE_ALL) # samples diff --git a/cmake/developer_package/packaging/rpm/rpm.cmake b/cmake/developer_package/packaging/rpm/rpm.cmake index 7c9fb4f22a372d..b7c482555bd131 100644 --- a/cmake/developer_package/packaging/rpm/rpm.cmake +++ b/cmake/developer_package/packaging/rpm/rpm.cmake @@ -73,6 +73,8 @@ macro(ov_define_component_include_rules) # tbb set(OV_CPACK_COMP_TBB_EXCLUDE_ALL EXCLUDE_FROM_ALL) set(OV_CPACK_COMP_TBB_DEV_EXCLUDE_ALL EXCLUDE_FROM_ALL) + # openmp + set(OV_CPACK_COMP_OPENMP_EXCLUDE_ALL EXCLUDE_FROM_ALL) # licensing set(OV_CPACK_COMP_LICENSING_EXCLUDE_ALL EXCLUDE_FROM_ALL) # samples diff --git a/cmake/features.cmake b/cmake/features.cmake index fa11b1894dfbba..d37c113e866ab8 100644 --- a/cmake/features.cmake +++ b/cmake/features.cmake @@ -85,8 +85,9 @@ ov_dependent_option (ENABLE_PKGCONFIG_GEN "Enable openvino.pc pkg-config file ge # # "OneDNN library based on OMP or TBB or Sequential implementation: TBB|OMP|SEQ" -if(RISCV64) +if(RISCV64 OR ANDROID) # oneDNN does not support non-SEQ for RISC-V architecture + # on Android we experience SEGFAULT during compilation set(THREADING_DEFAULT "SEQ") else() set(THREADING_DEFAULT "TBB") @@ -101,6 +102,17 @@ if(NOT THREADING IN_LIST THREADING_OPTIONS) message(FATAL_ERROR "THREADING should be set to either ${THREADING_OPTIONS}") endif() +if(X86_64 AND (WIN32 OR LINUX)) + # we have a precompiled version of Intel OMP only for this platforms + set(ENABLE_INTEL_OPENMP_DEFAULT ON) + # temporart override to OFF for testing purposes + set(ENABLE_INTEL_OPENMP_DEFAULT OFF) +else() + set(ENABLE_INTEL_OPENMP_DEFAULT OFF) +endif() + +ov_dependent_option (ENABLE_INTEL_OPENMP "Enables usage of Intel OpenMP instead of default compiler one" ${ENABLE_INTEL_OPENMP_DEFAULT} "THREADING STREQUAL SEQ" OFF) + if((THREADING STREQUAL "TBB" OR THREADING STREQUAL "TBB_AUTO") AND (BUILD_SHARED_LIBS OR (LINUX AND X86_64))) set(ENABLE_TBBBIND_2_5_DEFAULT ON) diff --git a/cmake/templates/OpenVINOConfig.cmake.in b/cmake/templates/OpenVINOConfig.cmake.in index af8dd1596c4a39..a276cf2c24eb66 100644 --- a/cmake/templates/OpenVINOConfig.cmake.in +++ b/cmake/templates/OpenVINOConfig.cmake.in @@ -10,6 +10,7 @@ # The following components are supported: # # * `Runtime`: OpenVINO C++ and C Core & Inference Runtime, frontend common +# * `Threading`: OpenVINO threading backend for parallel.hpp # * `JAX`: OpenVINO JAX frontend # * `ONNX`: OpenVINO ONNX frontend # * `Paddle`: OpenVINO Paddle frontend @@ -25,6 +26,10 @@ # # find_package(OpenVINO REQUIRED COMPONENTS Runtime ONNX) # +# If OpenVINO's openvino/core/parallel.hpp is required: +# +# find_package(OpenVINO REQUIRED COMPONENTS Runtime Threading) +# # Imported Targets: # ------ # @@ -150,9 +155,25 @@ macro(_ov_find_dependency dep) unset(cmake_fd_quiet_arg) endmacro() +macro(_ov_find_openmp) + set(_ov_threading "@THREADING@") + set(_ov_enable_intel_omp "@ENABLE_INTEL_OPENMP@") + + if(_ov_threading STREQUAL "OMP") + if(_ov_enable_intel_omp) + # TODO: write code to import iomp libraries + else() + _ov_find_dependency(OpenMP) + endif() + endif() + + unset(_ov_enable_intel_omp) + unset(_ov_threading) +endmacro() + macro(_ov_find_tbb) - set(THREADING "@THREADING@") - if(THREADING STREQUAL "TBB" OR THREADING STREQUAL "TBB_AUTO") + set(_ov_threading "@THREADING@") + if(_ov_threading STREQUAL "TBB" OR _ov_threading STREQUAL "TBB_AUTO") set(enable_pkgconfig_tbb "@tbb_FOUND@") # try tbb.pc @@ -224,6 +245,8 @@ macro(_ov_find_tbb) unset(enable_system_tbb) _ov_find_dependency(TBB + # we need to find at least the same version as we compiled with + @TBB_VERSION@ COMPONENTS tbb tbbmalloc ${find_package_tbb_extra_args}) unset(find_package_tbb_extra_args) @@ -245,6 +268,8 @@ macro(_ov_find_tbb) endif() unset(install_tbbbind) endif() + + unset(_ov_threading) endmacro() macro(_ov_find_pugixml) @@ -494,9 +519,14 @@ if(NOT TARGET openvino) endif() endif() +macro(_ov_enable_threading_interface) + _ov_find_tbb() + _ov_find_openmp() +endmacro() + if(NOT _OV_ENABLE_OPENVINO_BUILD_SHARED) # common openvino dependencies - _ov_find_tbb() + _ov_enable_threading_interface() _ov_find_itt() _ov_find_pugixml() @@ -549,6 +579,7 @@ endif() # set(${CMAKE_FIND_PACKAGE_NAME}_Runtime_FOUND ON) +set(${CMAKE_FIND_PACKAGE_NAME}_Threading_FOUND ON) set(${CMAKE_FIND_PACKAGE_NAME}_ONNX_FOUND @ENABLE_OV_ONNX_FRONTEND@) set(${CMAKE_FIND_PACKAGE_NAME}_Paddle_FOUND @ENABLE_OV_PADDLE_FRONTEND@) @@ -571,6 +602,23 @@ if(NOT ${CMAKE_FIND_PACKAGE_NAME}_FIND_COMPONENTS) set(${CMAKE_FIND_PACKAGE_NAME}_FIND_COMPONENTS Runtime) endif() +if(Threading IN_LIST ${CMAKE_FIND_PACKAGE_NAME}_FIND_COMPONENTS) + _ov_enable_threading_interface() + + if(TARGET TBB::tbb) + set(_ov_threading_lib TBB::tbb) + elseif(TARGET IntelOpenMP::OpenMP_CXX) + set(_ov_threading_lib IntelOpenMP::OpenMP_CXX) + elseif(TARGET OpenMP::OpenMP_CXX) + set(_ov_threading_lib OpenMP::OpenMP_CXX) + endif() + + if(_ov_threading_lib) + set_property(TARGET openvino::runtime APPEND PROPERTY INTERFACE_LINK_LIBRARIES ${_ov_threading_lib}) + unset(_ov_threading_lib) + endif() +endif() + # # Apply common functions # diff --git a/cmake/templates/OpenVINODeveloperPackageConfig.cmake.in b/cmake/templates/OpenVINODeveloperPackageConfig.cmake.in index fe3f82a513e08c..22eb432a97400a 100644 --- a/cmake/templates/OpenVINODeveloperPackageConfig.cmake.in +++ b/cmake/templates/OpenVINODeveloperPackageConfig.cmake.in @@ -70,11 +70,12 @@ find_dependency(OpenVINODeveloperScripts NO_DEFAULT_PATH) find_dependency(OpenVINO + COMPONENTS ${${CMAKE_FIND_PACKAGE_NAME}_FIND_COMPONENTS} PATHS "${CMAKE_CURRENT_LIST_DIR}" NO_CMAKE_FIND_ROOT_PATH NO_DEFAULT_PATH) -_ov_find_tbb() +_ov_enable_threading_interface() _ov_find_pugixml() include("${CMAKE_CURRENT_LIST_DIR}/openvino_developer_package_targets.cmake") diff --git a/cmake/templates/OpenVINODeveloperPackageConfigRelocatable.cmake.in b/cmake/templates/OpenVINODeveloperPackageConfigRelocatable.cmake.in index 613058cecc45a7..be8fc1fa802e23 100644 --- a/cmake/templates/OpenVINODeveloperPackageConfigRelocatable.cmake.in +++ b/cmake/templates/OpenVINODeveloperPackageConfigRelocatable.cmake.in @@ -52,14 +52,14 @@ set(CMAKE_COMPILE_WARNING_AS_ERROR OFF) # # OpenVINO_DIR is supposed to be set as an environment variable -find_dependency(OpenVINO) +find_dependency(OpenVINO COMPONENTS ${${CMAKE_FIND_PACKAGE_NAME}_FIND_COMPONENTS}) find_dependency(OpenVINODeveloperScripts PATHS "${CMAKE_CURRENT_LIST_DIR}" NO_CMAKE_FIND_ROOT_PATH NO_DEFAULT_PATH) -_ov_find_tbb() +_ov_enable_threading_interface() _ov_find_pugixml() include("${CMAKE_CURRENT_LIST_DIR}/OpenVINODeveloperPackageTargets.cmake") diff --git a/src/bindings/python/CMakeLists.txt b/src/bindings/python/CMakeLists.txt index dc816d73fa79b1..666b6bcd0d6969 100644 --- a/src/bindings/python/CMakeLists.txt +++ b/src/bindings/python/CMakeLists.txt @@ -251,7 +251,9 @@ endmacro() macro(ov_define_setup_py_dependencies) foreach(_target # Python API dependencies - _pyopenvino py_ov_frontends + pyopenvino py_ov_frontends + # C API + openvino_c # plugins ov_plugins # frontends diff --git a/src/bindings/python/wheel/CMakeLists.txt b/src/bindings/python/wheel/CMakeLists.txt index 97b5795c7a7e2c..b12921280c84c4 100644 --- a/src/bindings/python/wheel/CMakeLists.txt +++ b/src/bindings/python/wheel/CMakeLists.txt @@ -126,11 +126,6 @@ else() --plat-name=${PLATFORM_TAG}) endif() -# dev target includes c headers and we need to include c bindings -if(TARGET openvino_c) - list(APPEND ov_setup_py_deps openvino_c) -endif() - add_custom_command(OUTPUT ${openvino_wheel_path} COMMAND ${setup_py_env} ${wheel_build_command} diff --git a/src/bindings/python/wheel/setup.py b/src/bindings/python/wheel/setup.py index f88d9bd3da7592..b19c64f3d5d811 100644 --- a/src/bindings/python/wheel/setup.py +++ b/src/bindings/python/wheel/setup.py @@ -122,6 +122,12 @@ "rpath": LIBS_RPATH, "binary_dir": OPENVINO_BINARY_DIR, }, + "intel_openmp_libs": { + "name": "intel_omp", + "prefix": f"{BUILD_BASE}/libs.intel_omp", + "install_dir": OV_RUNTIME_LIBS_DIR, + "binary_dir": OPENVINO_BINARY_DIR, + }, "pugixml_libs": { "name": "pugixml", "prefix": f"{BUILD_BASE}/libs.pugixml", @@ -476,6 +482,8 @@ def copy_package_data(self, src_dirs): os.makedirs(package_cmake_dir, exist_ok=True) openvino_replacements = { + # change the location of TBBConfig.cmake (runtime/3rdparty/tbb -> openvino/cmake) + r"(\{PACKAGE_PREFIX_DIR\})\/runtime\/3rdparty\/tbb": rf"\1/{WHEEL_PACKAGE_DIR}/cmake", # change the path where the libraries are installed (runtime/lib/intel64/Release -> openvino/libs) r"(\{_IMPORT_PREFIX\})\/(.*)\/(.+\.[lib|dylib|so|dll])": rf"\1/{WHEEL_LIBS_INSTALL_DIR}/\3", # change the path where the include files are installed (runtime/include -> openvino/include) diff --git a/src/cmake/ov_parallel.cmake b/src/cmake/ov_parallel.cmake index cfb69ce7b1445f..51549b82baa6f6 100644 --- a/src/cmake/ov_parallel.cmake +++ b/src/cmake/ov_parallel.cmake @@ -249,6 +249,80 @@ macro(ov_find_package_tbb) endif() endmacro() +macro(ov_find_package_openmp) + # check whether the compiler supports OpenMP at all + find_package(OpenMP) + + # check if Intel OpenMP is downloaded and override system library + if(THREADING STREQUAL "OMP") + if(INTEL_OMP) + if(WIN32) + set(iomp_lib_name libiomp5md) + else() + set(iomp_lib_name iomp5) + endif() + + set(lib_rel_path ${INTEL_OMP}/lib) + set(lib_dbg_path ${lib_rel_path}) + + find_library(INTEL_OMP_LIBRARIES_RELEASE NAMES ${iomp_lib_name} PATHS ${lib_rel_path} REQUIRED NO_DEFAULT_PATH) + list(APPEND iomp_imported_configurations RELEASE) + + # try to find debug libraries as well + if(WIN32) + set(iomp_link_flags + # avoid linking default OpenMP + # https://learn.microsoft.com/en-us/cpp/parallel/openmp/reference/openmp-library-reference?view=msvc-170 + INTERFACE_LINK_OPTIONS -nodefaultlib:vcomp) + + # location of .lib file + string(REPLACE ".dll" ".lib" INTEL_OMP_IMPLIB_RELEASE "${INTEL_OMP_LIBRARIES_RELEASE}") + set(iomp_implib_location_release + IMPORTED_IMPLIB_RELEASE "${INTEL_OMP_IMPLIB_RELEASE}") + + find_library(INTEL_OMP_LIBRARIES_DEBUG NAMES ${iomp_lib_name} PATHS ${lib_dbg_path} NO_DEFAULT_PATH) + if(INTEL_OMP_LIBRARIES_DEBUG) + string(REPLACE ".dll" ".lib" INTEL_OMP_IMPLIB_DEBUG "${INTEL_OMP_LIBRARIES_DEBUG}") + + list(APPEND iomp_imported_configurations DEBUG) + set(iomp_imported_locations_debug + IMPORTED_LOCATION_DEBUG "${INTEL_OMP_LIBRARIES_DEBUG}" + IMPORTED_IMPLIB_DEBUG "${INTEL_OMP_IMPLIB_DEBUG}") + else() + set(iomp_map_imported_debug_configuration MAP_IMPORTED_CONFIG_DEBUG Release) + message(WARNING "OMP Debug binaries are missed.") + endif() + endif() + + # create imported target + if(NOT TARGET IntelOpenMP::OpenMP_CXX) + add_library(IntelOpenMP::OpenMP_CXX SHARED IMPORTED) + set_property(TARGET IntelOpenMP::OpenMP_CXX APPEND PROPERTY + IMPORTED_CONFIGURATIONS ${iomp_imported_configurations}) + set_target_properties(IntelOpenMP::OpenMP_CXX PROPERTIES + # reuse standard OpenMP compiler flags + INTERFACE_COMPILE_OPTIONS ${OpenMP_CXX_FLAGS} + # location of release library (both .dll and lib.) + IMPORTED_LOCATION_RELEASE "${INTEL_OMP_LIBRARIES_RELEASE}" + ${iomp_implib_location_release} + # the same for debug libs + ${iomp_imported_locations_debug} + # linker flags to override system OpenMP + ${iomp_link_flags} + # map imported configurations if required + ${iomp_map_imported_debug_configuration} + MAP_IMPORTED_CONFIG_MINSIZEREL Release + MAP_IMPORTED_CONFIG_RELWITHDEBINFO Release) + endif() + endif() + + # falling back to system OpenMP then + if(NOT TARGET IntelOpenMP::OpenMP_CXX) + _ov_target_link_libraries(${TARGET_NAME} ${LINK_TYPE} OpenMP::OpenMP_CXX) + endif() + endif() +endmacro() + function(ov_set_threading_interface_for TARGET_NAME) if(THREADING STREQUAL "TBB" OR THREADING STREQUAL "TBB_AUTO" AND NOT TBB_FOUND) # find TBB @@ -316,95 +390,33 @@ function(ov_set_threading_interface_for TARGET_NAME) set(OV_THREAD_DEFINE "OV_THREAD_SEQ") - if (THREADING STREQUAL "TBB" OR THREADING STREQUAL "TBB_AUTO") - if (TBB_FOUND) + if(THREADING STREQUAL "TBB" OR THREADING STREQUAL "TBB_AUTO") + if(TBB_FOUND) set(OV_THREAD_DEFINE "OV_THREAD_TBB") _ov_target_link_libraries(${TARGET_NAME} ${LINK_TYPE} TBB::tbb) target_compile_definitions(${TARGET_NAME} ${COMPILE_DEF_TYPE} TBB_PREVIEW_WAITING_FOR_WORKERS=1) - else () + else() set(THREADING "SEQ" PARENT_SCOPE) message(WARNING "TBB was not found by the configured TBB_DIR path.\ SEQ method will be used for ${TARGET_NAME}") - endif () - elseif (THREADING STREQUAL "OMP") - if (WIN32) - set(omp_lib_name libiomp5md) - elseif (ARM OR AARCH64) - get_filename_component(OpenMP_CXX_LIB_NAME ${OMP_LIB} NAME) - string(REGEX REPLACE "^lib" "" OpenMP_CXX_LIB_NAME ${OpenMP_CXX_LIB_NAME}) - string(REGEX REPLACE "\\.[^.]*$" "" OpenMP_CXX_LIB_NAME ${OpenMP_CXX_LIB_NAME}) - set(omp_lib_name ${OpenMP_CXX_LIB_NAME}) - else () - set(omp_lib_name iomp5) - endif () - - if (NOT OpenVINO_SOURCE_DIR) - # TODO: dead code since ov_parallel.cmake is not used outside of OpenVINO build - if (WIN32) - set(lib_rel_path ${IE_LIB_REL_DIR}) - set(lib_dbg_path ${IE_LIB_DBG_DIR}) - else () - set(lib_rel_path ${IE_EXTERNAL_DIR}/omp/lib) - set(lib_dbg_path ${lib_rel_path}) - endif () - else () - if (ARM OR AARCH64) - set(lib_rel_path ${OMP}) - else() - set(lib_rel_path ${OMP}/lib) - endif () - set(lib_dbg_path ${lib_rel_path}) - endif () - - if (NOT OMP_LIBRARIES_RELEASE) - find_library(OMP_LIBRARIES_RELEASE ${omp_lib_name} ${lib_rel_path} NO_DEFAULT_PATH) - message(STATUS "OMP Release lib: ${OMP_LIBRARIES_RELEASE}") - if (NOT LINUX) - find_library(OMP_LIBRARIES_DEBUG ${omp_lib_name} ${lib_dbg_path} NO_DEFAULT_PATH) - if (OMP_LIBRARIES_DEBUG) - message(STATUS "OMP Debug lib: ${OMP_LIBRARIES_DEBUG}") - else () - message(WARNING "OMP Debug binaries are missed.") - endif () - endif () - endif () + endif() + elseif(THREADING STREQUAL "OMP") + ov_find_package_openmp() - if (NOT OMP_LIBRARIES_RELEASE) - message(WARNING "Intel OpenMP not found. Intel OpenMP support will be disabled. ${OV_THREAD_DEFINE} is defined") - set(THREADING "SEQ" PARENT_SCOPE) - else () + if(TARGET IntelOpenMP::OpenMP_CXX) set(OV_THREAD_DEFINE "OV_THREAD_OMP") - - if (WIN32) - target_compile_options(${TARGET_NAME} ${LINK_TYPE} ${OpenMP_CXX_FLAGS} /openmp) - target_compile_options(${TARGET_NAME} ${LINK_TYPE} ${OpenMP_CXX_FLAGS} /Qopenmp) - _ov_target_link_libraries(${TARGET_NAME} ${LINK_TYPE} "-nodefaultlib:vcomp") - else() - target_compile_options(${TARGET_NAME} ${LINK_TYPE} ${OpenMP_CXX_FLAGS} -fopenmp) - endif () - - # Debug binaries are optional. - if (OMP_LIBRARIES_DEBUG AND NOT LINUX) - if (WIN32) - _ov_target_link_libraries(${TARGET_NAME} ${LINK_TYPE} "$<$:${OMP_LIBRARIES_DEBUG}>;$<$>:${OMP_LIBRARIES_RELEASE}>") - else() - # TODO: handle multi-config generators case - if (CMAKE_BUILD_TYPE STREQUAL "Debug") - _ov_target_link_libraries(${TARGET_NAME} ${LINK_TYPE} ${OMP_LIBRARIES_DEBUG}) - else() - _ov_target_link_libraries(${TARGET_NAME} ${LINK_TYPE} ${OMP_LIBRARIES_RELEASE}) - endif () - endif () - else () - # Link Release library to all configurations. - _ov_target_link_libraries(${TARGET_NAME} ${LINK_TYPE} ${OMP_LIBRARIES_RELEASE}) - endif () - endif () - endif () + _ov_target_link_libraries(${TARGET_NAME} ${LINK_TYPE} IntelOpenMP::OpenMP_CXX) + elseif(TARGET OpenMP::OpenMP_CXX) + set(OV_THREAD_DEFINE "OV_THREAD_OMP") + _ov_target_link_libraries(${TARGET_NAME} ${LINK_TYPE} OpenMP::OpenMP_CXX) + else() + message(FATAL_ERROR "Internal error: OpenMP is not supported by compiler. Switch to SEQ should be performed before") + endif() + endif() target_compile_definitions(${TARGET_NAME} ${COMPILE_DEF_TYPE} -DOV_THREAD=${OV_THREAD_DEFINE}) - if (NOT THREADING STREQUAL "SEQ") + if(NOT THREADING STREQUAL "SEQ") find_package(Threads REQUIRED) _ov_target_link_libraries(${TARGET_NAME} ${LINK_TYPE} Threads::Threads) endif() diff --git a/src/common/util/src/file_util.cpp b/src/common/util/src/file_util.cpp index 2846355e9aeb5f..c9bf331bbfe6d3 100644 --- a/src/common/util/src/file_util.cpp +++ b/src/common/util/src/file_util.cpp @@ -340,7 +340,7 @@ void ov::util::convert_path_win_style(std::string& path) { #ifdef OPENVINO_ENABLE_UNICODE_PATH_SUPPORT -# ifdef __APPLE__ +# ifdef __clang__ # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wdeprecated-declarations" # endif @@ -372,7 +372,7 @@ std::wstring ov::util::string_to_wstring(const std::string& string) { # endif } -# ifdef __APPLE__ +# ifdef __clang__ # pragma clang diagnostic pop # endif From c8839fa5339faa2b7cb1cfac1863717574c85d6b Mon Sep 17 00:00:00 2001 From: Maxim Vafin Date: Thu, 22 Aug 2024 22:50:15 +0200 Subject: [PATCH 25/35] [PT FE] Remove opset usage (#26181) ### Details: - *Remove usage of opset* ### Tickets: - *ticket-id* --- .../frontend/pytorch/node_context.hpp | 12 +- src/frontends/pytorch/src/node_context.cpp | 6 +- src/frontends/pytorch/src/op/log_softmax.cpp | 8 +- .../src/op/native_multi_head_attention.cpp | 181 ++++++------- .../transforms/prim_list_unpack_replacer.cpp | 237 +++++++++--------- src/frontends/pytorch/src/utils.cpp | 130 +++++----- 6 files changed, 303 insertions(+), 271 deletions(-) diff --git a/src/frontends/pytorch/include/openvino/frontend/pytorch/node_context.hpp b/src/frontends/pytorch/include/openvino/frontend/pytorch/node_context.hpp index 110beedb8b2020..d97c2ef694a1eb 100644 --- a/src/frontends/pytorch/include/openvino/frontend/pytorch/node_context.hpp +++ b/src/frontends/pytorch/include/openvino/frontend/pytorch/node_context.hpp @@ -53,13 +53,21 @@ class NodeContext : public frontend::NodeContext { // TODO: int due to base class uses it, but naturally it should be size_t for PT Output get_input(int index) const override { size_t index_ = static_cast(index); - FRONT_END_GENERAL_CHECK(!m_decoder->input_is_none(index_), "Input doesn't exist with index: ", index); + FRONT_END_GENERAL_CHECK(!m_decoder->input_is_none(index_), + "Input doesn't exist with index: ", + index, + " for operation ", + get_op_type()); auto input = m_decoder_inputs.at(index); if (input == 0) { // Case when input can be inlined (possible only for fx decoder) if (m_decoder->is_input_inlined(index_)) { auto inlined_input = m_decoder->inlined_input(index_); - FRONT_END_GENERAL_CHECK(inlined_input.size() == 1, "Incorrect inlined input with index:", index); + FRONT_END_GENERAL_CHECK(inlined_input.size() == 1, + "Incorrect inlined input with index: ", + index, + " for operation ", + get_op_type()); return inlined_input[0]; } } diff --git a/src/frontends/pytorch/src/node_context.cpp b/src/frontends/pytorch/src/node_context.cpp index efc3bb107eb7f0..565b0cdbd39385 100644 --- a/src/frontends/pytorch/src/node_context.cpp +++ b/src/frontends/pytorch/src/node_context.cpp @@ -152,7 +152,11 @@ OutputVector NodeContext::inputs() const { // Case when input can be inlined (possible only for fx decoder) if (m_decoder->is_input_inlined(i)) { auto inlined_input = m_decoder->inlined_input(i); - FRONT_END_GENERAL_CHECK(inlined_input.size() == 1, "Incorrect inlined input with index:", i); + FRONT_END_GENERAL_CHECK(inlined_input.size() == 1, + "Incorrect inlined input with index: ", + i, + " for operation ", + get_op_type()); res.push_back(inlined_input[0]); continue; } diff --git a/src/frontends/pytorch/src/op/log_softmax.cpp b/src/frontends/pytorch/src/op/log_softmax.cpp index 6fb2e1c6be3241..500df2bf36bc20 100644 --- a/src/frontends/pytorch/src/op/log_softmax.cpp +++ b/src/frontends/pytorch/src/op/log_softmax.cpp @@ -2,8 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 // +#include "openvino/op/log_softmax.hpp" + #include "openvino/frontend/pytorch/node_context.hpp" -#include "openvino/opsets/opset10.hpp" +#include "openvino/op/convert.hpp" #include "utils.hpp" namespace ov { @@ -30,11 +32,11 @@ OutputVector translate_log_softmax_common(const NodeContext& context, bool is_fx const auto target_dtype_i64 = context.const_input(2); const auto target_dtype = convert_dtype(target_dtype_i64); if (elem_type != target_dtype) { - input = context.mark_node(std::make_shared(input, target_dtype)); + input = context.mark_node(std::make_shared(input, target_dtype)); } } - const auto log_softmax = context.mark_node(std::make_shared(input, dim)); + const auto log_softmax = context.mark_node(std::make_shared(input, dim)); return {log_softmax}; }; diff --git a/src/frontends/pytorch/src/op/native_multi_head_attention.cpp b/src/frontends/pytorch/src/op/native_multi_head_attention.cpp index 3da7a0e5402669..cb8e33750d9a22 100644 --- a/src/frontends/pytorch/src/op/native_multi_head_attention.cpp +++ b/src/frontends/pytorch/src/op/native_multi_head_attention.cpp @@ -3,7 +3,23 @@ // #include "openvino/frontend/pytorch/node_context.hpp" -#include "openvino/opsets/opset10.hpp" +#include "openvino/op/add.hpp" +#include "openvino/op/broadcast.hpp" +#include "openvino/op/concat.hpp" +#include "openvino/op/divide.hpp" +#include "openvino/op/gather.hpp" +#include "openvino/op/logical_not.hpp" +#include "openvino/op/matmul.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/reduce_sum.hpp" +#include "openvino/op/reshape.hpp" +#include "openvino/op/select.hpp" +#include "openvino/op/shape_of.hpp" +#include "openvino/op/slice.hpp" +#include "openvino/op/softmax.hpp" +#include "openvino/op/sqrt.hpp" +#include "openvino/op/transpose.hpp" +#include "openvino/op/unsqueeze.hpp" #include "pt_framework_node.hpp" #include "utils.hpp" @@ -46,77 +62,69 @@ OutputVector translate_native_multi_head_attention(const NodeContext& context) { const auto average_weights = context.const_input(11); const auto minus_inf = - context.mark_node(opset10::Constant::create(element::f32, Shape{}, {-std::numeric_limits::infinity()})); - const auto embed_dim_i64 = context.mark_node(std::make_shared(embed_dim, element::i64)); - const auto num_head_i64 = context.mark_node(std::make_shared(num_head, element::i64)); - - const auto neg_one_1d = context.mark_node(opset10::Constant::create(element::i64, Shape{1}, {-1})); - const auto zero_1d = context.mark_node(opset10::Constant::create(element::i64, Shape{1}, {0})); - const auto one_1d = context.mark_node(opset10::Constant::create(element::i64, Shape{1}, {1})); - const auto two_1d = context.mark_node(opset10::Constant::create(element::i64, Shape{1}, {2})); - const auto three_1d = context.mark_node(opset10::Constant::create(element::i64, Shape{1}, {3})); - const auto heads_1d = context.mark_node(std::make_shared(num_head_i64, zero_1d)); - - const auto ev_1_slice_1d = context.mark_node(std::make_shared(one_1d, embed_dim_i64)); - const auto ev_2_slice_1d = context.mark_node(std::make_shared(two_1d, embed_dim_i64)); - const auto ev_3_slice_1d = context.mark_node(std::make_shared(three_1d, embed_dim_i64)); - - const auto qkv_shape = context.mark_node(std::make_shared(query)); - const auto batch_size = context.mark_node(std::make_shared(qkv_shape, zero_1d, zero_1d)); - const auto seq_size = context.mark_node(std::make_shared(qkv_shape, one_1d, zero_1d)); - const auto embed_div_heads = context.mark_node(std::make_shared(embed_dim_i64, heads_1d, true)); + context.mark_node(v0::Constant::create(element::f32, Shape{}, {-std::numeric_limits::infinity()})); + const auto embed_dim_i64 = context.mark_node(std::make_shared(embed_dim, element::i64)); + const auto num_head_i64 = context.mark_node(std::make_shared(num_head, element::i64)); + + const auto neg_one_1d = context.mark_node(v0::Constant::create(element::i64, Shape{1}, {-1})); + const auto zero_1d = context.mark_node(v0::Constant::create(element::i64, Shape{1}, {0})); + const auto one_1d = context.mark_node(v0::Constant::create(element::i64, Shape{1}, {1})); + const auto two_1d = context.mark_node(v0::Constant::create(element::i64, Shape{1}, {2})); + const auto three_1d = context.mark_node(v0::Constant::create(element::i64, Shape{1}, {3})); + const auto heads_1d = context.mark_node(std::make_shared(num_head_i64, zero_1d)); + + const auto ev_1_slice_1d = context.mark_node(std::make_shared(one_1d, embed_dim_i64)); + const auto ev_2_slice_1d = context.mark_node(std::make_shared(two_1d, embed_dim_i64)); + const auto ev_3_slice_1d = context.mark_node(std::make_shared(three_1d, embed_dim_i64)); + + const auto qkv_shape = context.mark_node(std::make_shared(query)); + const auto batch_size = context.mark_node(std::make_shared(qkv_shape, zero_1d, zero_1d)); + const auto seq_size = context.mark_node(std::make_shared(qkv_shape, one_1d, zero_1d)); + const auto embed_div_heads = context.mark_node(std::make_shared(embed_dim_i64, heads_1d, true)); const auto query_proj_weight = - context.mark_node(std::make_shared(qkv_weight, zero_1d, ev_1_slice_1d, one_1d, zero_1d)); + context.mark_node(std::make_shared(qkv_weight, zero_1d, ev_1_slice_1d, one_1d, zero_1d)); const auto key_proj_weight = - context.mark_node(std::make_shared(qkv_weight, ev_1_slice_1d, ev_2_slice_1d, one_1d, zero_1d)); + context.mark_node(std::make_shared(qkv_weight, ev_1_slice_1d, ev_2_slice_1d, one_1d, zero_1d)); const auto value_proj_weight = - context.mark_node(std::make_shared(qkv_weight, ev_2_slice_1d, ev_3_slice_1d, one_1d, zero_1d)); + context.mark_node(std::make_shared(qkv_weight, ev_2_slice_1d, ev_3_slice_1d, one_1d, zero_1d)); const auto query_proj_bias = - context.mark_node(std::make_shared(qkv_bias, zero_1d, ev_1_slice_1d, one_1d, zero_1d)); + context.mark_node(std::make_shared(qkv_bias, zero_1d, ev_1_slice_1d, one_1d, zero_1d)); const auto key_proj_bias = - context.mark_node(std::make_shared(qkv_bias, ev_1_slice_1d, ev_2_slice_1d, one_1d, zero_1d)); + context.mark_node(std::make_shared(qkv_bias, ev_1_slice_1d, ev_2_slice_1d, one_1d, zero_1d)); const auto value_proj_bias = - context.mark_node(std::make_shared(qkv_bias, ev_2_slice_1d, ev_3_slice_1d, one_1d, zero_1d)); - - const auto query_weighted = - context.mark_node(std::make_shared(query, query_proj_weight, false, true)); - const auto key_weighted = context.mark_node(std::make_shared(key, key_proj_weight, false, true)); - const auto value_weighted = - context.mark_node(std::make_shared(value, value_proj_weight, false, true)); - - const auto query_biased = context.mark_node(std::make_shared(query_weighted, query_proj_bias)); - const auto key_biased = context.mark_node(std::make_shared(key_weighted, key_proj_bias)); - const auto value_biased = context.mark_node(std::make_shared(value_weighted, value_proj_bias)); - - const auto qkv_reshape_dims = context.mark_node( - std::make_shared(OutputVector{batch_size, seq_size, heads_1d, neg_one_1d}, 0)); - const auto qv_transpose_dims = context.mark_node(opset10::Constant::create(element::i64, Shape{4}, {0, 2, 1, 3})); - const auto k_transpose_dims = context.mark_node(opset10::Constant::create(element::i64, Shape{4}, {0, 2, 3, 1})); - - const auto query_reshaped = - context.mark_node(std::make_shared(query_biased, qkv_reshape_dims, false)); - const auto key_reshaped = - context.mark_node(std::make_shared(key_biased, qkv_reshape_dims, false)); - const auto value_reshaped = - context.mark_node(std::make_shared(value_biased, qkv_reshape_dims, false)); - - const auto query_transposed = - context.mark_node(std::make_shared(query_reshaped, qv_transpose_dims)); - const auto key_transposed = context.mark_node(std::make_shared(key_reshaped, k_transpose_dims)); - const auto value_transposed = - context.mark_node(std::make_shared(value_reshaped, qv_transpose_dims)); - - const auto scale_one = context.mark_node(std::make_shared(one_1d, query_transposed)); - const auto scale_dim = context.mark_node(std::make_shared(embed_div_heads, query_transposed)); - const auto scale_dim_sqrt = context.mark_node(std::make_shared(scale_dim)); - const auto scale = context.mark_node(std::make_shared(scale_one, scale_dim_sqrt)); + context.mark_node(std::make_shared(qkv_bias, ev_2_slice_1d, ev_3_slice_1d, one_1d, zero_1d)); + + const auto query_weighted = context.mark_node(std::make_shared(query, query_proj_weight, false, true)); + const auto key_weighted = context.mark_node(std::make_shared(key, key_proj_weight, false, true)); + const auto value_weighted = context.mark_node(std::make_shared(value, value_proj_weight, false, true)); + + const auto query_biased = context.mark_node(std::make_shared(query_weighted, query_proj_bias)); + const auto key_biased = context.mark_node(std::make_shared(key_weighted, key_proj_bias)); + const auto value_biased = context.mark_node(std::make_shared(value_weighted, value_proj_bias)); + + const auto qkv_reshape_dims = + context.mark_node(std::make_shared(OutputVector{batch_size, seq_size, heads_1d, neg_one_1d}, 0)); + const auto qv_transpose_dims = context.mark_node(v0::Constant::create(element::i64, Shape{4}, {0, 2, 1, 3})); + const auto k_transpose_dims = context.mark_node(v0::Constant::create(element::i64, Shape{4}, {0, 2, 3, 1})); + + const auto query_reshaped = context.mark_node(std::make_shared(query_biased, qkv_reshape_dims, false)); + const auto key_reshaped = context.mark_node(std::make_shared(key_biased, qkv_reshape_dims, false)); + const auto value_reshaped = context.mark_node(std::make_shared(value_biased, qkv_reshape_dims, false)); + + const auto query_transposed = context.mark_node(std::make_shared(query_reshaped, qv_transpose_dims)); + const auto key_transposed = context.mark_node(std::make_shared(key_reshaped, k_transpose_dims)); + const auto value_transposed = context.mark_node(std::make_shared(value_reshaped, qv_transpose_dims)); + + const auto scale_one = context.mark_node(std::make_shared(one_1d, query_transposed)); + const auto scale_dim = context.mark_node(std::make_shared(embed_div_heads, query_transposed)); + const auto scale_dim_sqrt = context.mark_node(std::make_shared(scale_dim)); + const auto scale = context.mark_node(std::make_shared(scale_one, scale_dim_sqrt)); const auto query_key_transpose_dot_product = - context.mark_node(std::make_shared(query_transposed, key_transposed)); + context.mark_node(std::make_shared(query_transposed, key_transposed)); - auto scaled_dot_product = - context.mark_node(std::make_shared(query_key_transpose_dot_product, scale)); + auto scaled_dot_product = context.mark_node(std::make_shared(query_key_transpose_dot_product, scale)); // Mask handling if (!context.input_is_none(9) && !context.input_is_none(12)) { @@ -125,61 +133,58 @@ OutputVector translate_native_multi_head_attention(const NodeContext& context) { // non-boolean to boolean masks if (atten_mask.get_element_type() == element::boolean) { const auto minus_inf_conv = - context.mark_node(std::make_shared(minus_inf, scaled_dot_product)); - const auto mask_inverse = context.mark_node(std::make_shared(atten_mask)); - atten_mask = context.mark_node(std::make_shared(atten_mask, scaled_dot_product)); - atten_mask = context.mark_node(std::make_shared(mask_inverse, atten_mask, minus_inf_conv)); + context.mark_node(std::make_shared(minus_inf, scaled_dot_product)); + const auto mask_inverse = context.mark_node(std::make_shared(atten_mask)); + atten_mask = context.mark_node(std::make_shared(atten_mask, scaled_dot_product)); + atten_mask = context.mark_node(std::make_shared(mask_inverse, atten_mask, minus_inf_conv)); } else { // Once int/float mask type is supported in PyTorch, // remove this assert to allow for such masks in OV PYTORCH_OP_CONVERSION_CHECK(1, "Non-boolean masks are not supported."); - atten_mask = context.mark_node(std::make_shared(atten_mask, scaled_dot_product)); + atten_mask = context.mark_node(std::make_shared(atten_mask, scaled_dot_product)); } // If mask type is 1 (only key-padding) then mask's shape is (N, S). // It must be reshaped to (N, 1, 1, S) to properly broadcast it proper dims in the next step if (context.const_input(12) == 1) { - const auto target_mask_reshape = context.mark_node( - std::make_shared(OutputVector{batch_size, one_1d, one_1d, seq_size}, 0)); - atten_mask = context.mark_node(std::make_shared(atten_mask, target_mask_reshape, false)); + const auto target_mask_reshape = + context.mark_node(std::make_shared(OutputVector{batch_size, one_1d, one_1d, seq_size}, 0)); + atten_mask = context.mark_node(std::make_shared(atten_mask, target_mask_reshape, false)); } // All mask types should be broadcast to this shape, // Except for type 2 which already has this shape if (context.const_input(12) != 2) { const auto target_mask_shape = context.mark_node( - std::make_shared(OutputVector{batch_size, heads_1d, seq_size, seq_size}, 0)); - atten_mask = context.mark_node(std::make_shared(atten_mask, target_mask_shape)); + std::make_shared(OutputVector{batch_size, heads_1d, seq_size, seq_size}, 0)); + atten_mask = context.mark_node(std::make_shared(atten_mask, target_mask_shape)); } - scaled_dot_product = context.mark_node(std::make_shared(scaled_dot_product, atten_mask)); + scaled_dot_product = context.mark_node(std::make_shared(scaled_dot_product, atten_mask)); } - const auto scaled_dot_product_softmax = - context.mark_node(std::make_shared(scaled_dot_product, -1)); + const auto scaled_dot_product_softmax = context.mark_node(std::make_shared(scaled_dot_product, -1)); const auto scaled_dot_product_attention = - context.mark_node(std::make_shared(scaled_dot_product_softmax, value_transposed)); + context.mark_node(std::make_shared(scaled_dot_product_softmax, value_transposed)); const auto sdp_reshape_dims = - context.mark_node(std::make_shared(OutputVector{batch_size, seq_size, neg_one_1d}, 0)); + context.mark_node(std::make_shared(OutputVector{batch_size, seq_size, neg_one_1d}, 0)); // Undo transpose (transpose back to original qv shape) const auto scaled_dot_product_attention_transposed = - context.mark_node(std::make_shared(scaled_dot_product_attention, qv_transpose_dims)); + context.mark_node(std::make_shared(scaled_dot_product_attention, qv_transpose_dims)); const auto scaled_dot_product_attention_reshaped = context.mark_node( - std::make_shared(scaled_dot_product_attention_transposed, sdp_reshape_dims, false)); + std::make_shared(scaled_dot_product_attention_transposed, sdp_reshape_dims, false)); const auto scaled_dot_product_attention_weighted = context.mark_node( - std::make_shared(scaled_dot_product_attention_reshaped, proj_weight, false, true)); + std::make_shared(scaled_dot_product_attention_reshaped, proj_weight, false, true)); const auto scaled_dot_product_attention_biased = - context.mark_node(std::make_shared(scaled_dot_product_attention_weighted, proj_bias)); + context.mark_node(std::make_shared(scaled_dot_product_attention_weighted, proj_bias)); if (average_weights) { - const auto target_div_shape = context.mark_node(std::make_shared(scaled_dot_product)); - const auto heads_div = context.mark_node(std::make_shared(heads_1d, target_div_shape)); - const auto heads_div_conv = - context.mark_node(std::make_shared(heads_div, scaled_dot_product)); - scaled_dot_product = - context.mark_node(std::make_shared(scaled_dot_product, heads_div_conv, false)); - scaled_dot_product = context.mark_node(std::make_shared(scaled_dot_product, one_1d)); + const auto target_div_shape = context.mark_node(std::make_shared(scaled_dot_product)); + const auto heads_div = context.mark_node(std::make_shared(heads_1d, target_div_shape)); + const auto heads_div_conv = context.mark_node(std::make_shared(heads_div, scaled_dot_product)); + scaled_dot_product = context.mark_node(std::make_shared(scaled_dot_product, heads_div_conv, false)); + scaled_dot_product = context.mark_node(std::make_shared(scaled_dot_product, one_1d)); } if (need_weights) { diff --git a/src/frontends/pytorch/src/transforms/prim_list_unpack_replacer.cpp b/src/frontends/pytorch/src/transforms/prim_list_unpack_replacer.cpp index 27d1245d475c56..5560c9ff225e9d 100644 --- a/src/frontends/pytorch/src/transforms/prim_list_unpack_replacer.cpp +++ b/src/frontends/pytorch/src/transforms/prim_list_unpack_replacer.cpp @@ -8,8 +8,25 @@ #include #include "openvino/core/rt_info.hpp" +#include "openvino/op/add.hpp" +#include "openvino/op/broadcast.hpp" +#include "openvino/op/concat.hpp" +#include "openvino/op/divide.hpp" +#include "openvino/op/gather.hpp" +#include "openvino/op/greater.hpp" +#include "openvino/op/mod.hpp" +#include "openvino/op/non_zero.hpp" +#include "openvino/op/range.hpp" +#include "openvino/op/reshape.hpp" +#include "openvino/op/shape_of.hpp" +#include "openvino/op/slice.hpp" +#include "openvino/op/split.hpp" +#include "openvino/op/squeeze.hpp" +#include "openvino/op/subtract.hpp" +#include "openvino/op/tile.hpp" +#include "openvino/op/unsqueeze.hpp" #include "openvino/op/util/framework_node.hpp" -#include "openvino/opsets/opset10.hpp" +#include "openvino/op/variadic_split.hpp" #include "openvino/pass/pattern/matcher.hpp" #include "openvino/pass/pattern/op/wrap_type.hpp" #include "utils.hpp" @@ -19,6 +36,8 @@ namespace frontend { namespace pytorch { namespace pass { +using namespace ov::op; + PrimListUnpackReplacer::PrimListUnpackReplacer() { auto list_unpack = ov::pass::pattern::wrap_type(); @@ -41,213 +60,195 @@ PrimListUnpackReplacer::PrimListUnpackReplacer() { // allow for last chunk to be smaller if data is not equally divisible. auto split_size = torch_split->get_input_source_output(1); // Using number of ListUnpack outputs. - auto num_out_m_1 = opset10::Constant::create(split_size.get_element_type(), - Shape{1}, - {list_unpack->get_output_size() - 1}); - auto const_neg_1 = opset10::Constant::create(split_size.get_element_type(), Shape{1}, {-1}); - auto split_lenghts_m_1 = rg.make(split_size, num_out_m_1); + auto num_out_m_1 = + v0::Constant::create(split_size.get_element_type(), Shape{1}, {list_unpack->get_output_size() - 1}); + auto const_neg_1 = v0::Constant::create(split_size.get_element_type(), Shape{1}, {-1}); + auto split_lenghts_m_1 = rg.make(split_size, num_out_m_1); NodeVector concat_inputs{split_lenghts_m_1, const_neg_1}; - auto split_lenghts = rg.make(concat_inputs, 0); - split = rg.make(torch_split->get_input_source_output(0), - torch_split->get_input_source_output(2), - split_lenghts); + auto split_lenghts = rg.make(concat_inputs, 0); + split = rg.make(torch_split->get_input_source_output(0), + torch_split->get_input_source_output(2), + split_lenghts); } else { - split = rg.make(torch_split->get_input_source_output(0), - torch_split->get_input_source_output(2), - torch_split->get_input_source_output(1)); + split = rg.make(torch_split->get_input_source_output(0), + torch_split->get_input_source_output(2), + torch_split->get_input_source_output(1)); } copy_runtime_info_and_name(list_unpack, rg.get(), {input_node}); replace_node(list_unpack, split); return true; - } - - if (auto split_with_sizes = cast_fw_node(input_node, "aten::split_with_sizes")) { + } else if (auto split_with_sizes = cast_fw_node(input_node, "aten::split_with_sizes")) { auto split_lengths = concat_list_construct(split_with_sizes->get_input_source_output(1)); - auto split = rg.make(split_with_sizes->get_input_source_output(0), - split_with_sizes->get_input_source_output(2), - split_lengths); + auto split = rg.make(split_with_sizes->get_input_source_output(0), + split_with_sizes->get_input_source_output(2), + split_lengths); copy_runtime_info_and_name(list_unpack, rg.get(), {input_node}); replace_node(list_unpack, split); return true; - } - - if (auto chunk = cast_fw_node(input_node, "aten::chunk")) { + } else if (auto chunk = cast_fw_node(input_node, "aten::chunk")) { if (list_unpack->get_output_size() == 1) { list_unpack->output(0).replace(input_node->input_value(0)); return true; } auto input_tensor = chunk->get_input_source_output(0); auto chunks = chunk->get_input_source_output(1); - chunks = rg.make(chunks, element::i32); + chunks = rg.make(chunks, element::i32); auto dim = chunk->get_input_source_output(2); - auto tensor_0 = opset10::Constant::create(element::i32, Shape{1}, {0}); - auto tensor_neg_1 = opset10::Constant::create(element::i32, Shape{1}, {-1}); + auto tensor_0 = v0::Constant::create(element::i32, Shape{1}, {0}); + auto tensor_neg_1 = v0::Constant::create(element::i32, Shape{1}, {-1}); - auto input_shape = rg.make(input_tensor, element::i32); - auto input_dimension = rg.make(input_shape, dim, tensor_0); + auto input_shape = rg.make(input_tensor, element::i32); + auto input_dimension = rg.make(input_shape, dim, tensor_0); - auto init_chunk_size = rg.make(input_dimension, chunks, true); + auto init_chunk_size = rg.make(input_dimension, chunks, true); // Add 1 if input is not evenly divisible by chunks - auto last_chunk_size = rg.make(input_dimension, chunks); - auto is_last_nonzero = rg.make(last_chunk_size, tensor_0); - auto is_last_nonzero_int = rg.make(is_last_nonzero, element::i32); + auto last_chunk_size = rg.make(input_dimension, chunks); + auto is_last_nonzero = rg.make(last_chunk_size, tensor_0); + auto is_last_nonzero_int = rg.make(is_last_nonzero, element::i32); - auto chunk_size = rg.make(init_chunk_size, is_last_nonzero_int); + auto chunk_size = rg.make(init_chunk_size, is_last_nonzero_int); auto split_lengths_even_size = - opset10::Constant::create(element::i32, Shape{1}, {list_unpack->get_output_size() - 1}); - auto split_lengths_even = rg.make(chunk_size, split_lengths_even_size); + v0::Constant::create(element::i32, Shape{1}, {list_unpack->get_output_size() - 1}); + auto split_lengths_even = rg.make(chunk_size, split_lengths_even_size); - auto split_lengths = rg.make(OutputVector{split_lengths_even, tensor_neg_1}, 0); - auto sliced_chunks = rg.make(input_tensor, dim, split_lengths); + auto split_lengths = rg.make(OutputVector{split_lengths_even, tensor_neg_1}, 0); + auto sliced_chunks = rg.make(input_tensor, dim, split_lengths); copy_runtime_info_and_name(list_unpack, rg.get(), {input_node}); replace_node(list_unpack, sliced_chunks); return true; - } - - if (auto tensor_split = cast_fw_node(input_node, "aten::tensor_split")) { + } else if (auto tensor_split = cast_fw_node(input_node, "aten::tensor_split")) { auto rank = tensor_split->input(1).get_partial_shape().rank(); if (rank.is_dynamic()) { add_exception_to_fw_node(tensor_split, "aten::tensor_split: dynamic rank is not supported."); return false; } - auto const_0 = opset10::Constant::create(element::i32, Shape{1}, {0}); - auto const_1 = opset10::Constant::create(element::i32, Shape{1}, {1}); - auto const_0_scalar = opset10::Constant::create(element::i32, Shape{}, {0}); - auto const_1_scalar = opset10::Constant::create(element::i32, Shape{}, {1}); - auto const_max = opset10::Constant::create(element::i32, Shape{1}, {std::numeric_limits::max()}); - auto const_neg_1 = opset10::Constant::create(element::i32, Shape{1}, {-1}); + auto const_0 = v0::Constant::create(element::i32, Shape{1}, {0}); + auto const_1 = v0::Constant::create(element::i32, Shape{1}, {1}); + auto const_0_scalar = v0::Constant::create(element::i32, Shape{}, {0}); + auto const_1_scalar = v0::Constant::create(element::i32, Shape{}, {1}); + auto const_max = v0::Constant::create(element::i32, Shape{1}, {std::numeric_limits::max()}); + auto const_neg_1 = v0::Constant::create(element::i32, Shape{1}, {-1}); auto input = tensor_split->get_input_source_output(0); auto indices_or_sections = tensor_split->get_input_source_output(1); - indices_or_sections = std::make_shared(indices_or_sections, element::i32); - auto dim = rg.make(tensor_split->get_input_source_output(2), const_0); - auto list_num_outs = opset10::Constant::create(element::i32, Shape{1}, {list_unpack->get_output_size()}); - auto list_num_outs_scalar = - opset10::Constant::create(element::i32, Shape{}, {list_unpack->get_output_size()}); + indices_or_sections = std::make_shared(indices_or_sections, element::i32); + auto dim = rg.make(tensor_split->get_input_source_output(2), const_0); + auto list_num_outs = v0::Constant::create(element::i32, Shape{1}, {list_unpack->get_output_size()}); + auto list_num_outs_scalar = v0::Constant::create(element::i32, Shape{}, {list_unpack->get_output_size()}); if (rank.get_length() == 0) { - auto input_shape = rg.make(input, element::i32); - auto axis_size = rg.make(input_shape, dim, const_0); - auto minimum_split_size = rg.make(axis_size, indices_or_sections); - auto maximum_split_size = rg.make(minimum_split_size, const_1); - auto num_splits_with_max_size = rg.make(axis_size, indices_or_sections); - auto num_splits_with_min_size = - rg.make(indices_or_sections, num_splits_with_max_size); - auto splits_max_size = rg.make(maximum_split_size, num_splits_with_max_size); - auto splits_min_size = rg.make(minimum_split_size, num_splits_with_min_size); - - auto split_sizes = rg.make(OutputVector{splits_max_size, splits_min_size}, 0); + auto input_shape = rg.make(input, element::i32); + auto axis_size = rg.make(input_shape, dim, const_0); + auto minimum_split_size = rg.make(axis_size, indices_or_sections); + auto maximum_split_size = rg.make(minimum_split_size, const_1); + auto num_splits_with_max_size = rg.make(axis_size, indices_or_sections); + auto num_splits_with_min_size = rg.make(indices_or_sections, num_splits_with_max_size); + auto splits_max_size = rg.make(maximum_split_size, num_splits_with_max_size); + auto splits_min_size = rg.make(minimum_split_size, num_splits_with_min_size); + + auto split_sizes = rg.make(OutputVector{splits_max_size, splits_min_size}, 0); // Reshape is used to make number of outputs static. - auto split_sizes_known_length = rg.make(split_sizes, list_num_outs, false); - auto splits = rg.make(input, dim, split_sizes_known_length); + auto split_sizes_known_length = rg.make(split_sizes, list_num_outs, false); + auto splits = rg.make(input, dim, split_sizes_known_length); copy_runtime_info_and_name(list_unpack, rg.get(), {input_node}); replace_node(list_unpack, splits->outputs()); return true; } else { - auto range = - rg.make(const_0_scalar, list_num_outs_scalar, const_1_scalar, element::i32); - auto range_plus_1 = rg.make(range, const_1); + auto range = rg.make(const_0_scalar, list_num_outs_scalar, const_1_scalar, element::i32); + auto range_plus_1 = rg.make(range, const_1); auto sections = - rg.make(OutputVector{const_0, std::move(indices_or_sections), const_max}, 0); + rg.make(OutputVector{const_0, std::move(indices_or_sections), const_max}, 0); - auto starts_tensor = rg.make(sections, const_0, const_neg_1, const_1, const_0); + auto starts_tensor = rg.make(sections, const_0, const_neg_1, const_1, const_0); auto starts = - rg.make(starts_tensor, const_0_scalar, list_unpack->get_output_size())->outputs(); - auto stops_tensor = rg.make(sections, const_1, const_max, const_1, const_0); + rg.make(starts_tensor, const_0_scalar, list_unpack->get_output_size())->outputs(); + auto stops_tensor = rg.make(sections, const_1, const_max, const_1, const_0); auto stops = - rg.make(stops_tensor, const_0_scalar, list_unpack->get_output_size())->outputs(); + rg.make(stops_tensor, const_0_scalar, list_unpack->get_output_size())->outputs(); OutputVector outputs{}; for (size_t i = 0; i < list_unpack->get_output_size(); i++) { - auto slice = rg.make(input, starts[i], stops[i], const_1, dim); + auto slice = rg.make(input, starts[i], stops[i], const_1, dim); outputs.push_back(slice); } copy_runtime_info_and_name(list_unpack, rg.get(), {input_node}); replace_node(list_unpack, outputs); return true; } - } - - if (auto broadcast_tensors = cast_fw_node(input_node, "aten::broadcast_tensors")) { + } else if (auto broadcast_tensors = cast_fw_node(input_node, "aten::broadcast_tensors")) { auto tensors = cast_fw_node(broadcast_tensors->input_value(0).get_node_shared_ptr(), "prim::ListConstruct"); if (!tensors) { add_exception_to_fw_node(input_node, "aten::broadcast_tensors: only prim::ListConstruct supported as input."); return false; } - Output final_shape_t = opset10::Constant::create(element::i32, Shape{}, {0}); + Output final_shape_t = v0::Constant::create(element::i32, Shape{}, {0}); for (auto& input : tensors->inputs()) { - auto tensor_shape = rg.make(input.get_source_output(), element::i32); + auto tensor_shape = rg.make(input.get_source_output(), element::i32); final_shape_t = - rg.make(final_shape_t, tensor_shape, ov::op::BroadcastType::BIDIRECTIONAL); + rg.make(final_shape_t, tensor_shape, ov::op::BroadcastType::BIDIRECTIONAL); } - auto final_shape = rg.make(final_shape_t, element::i32); + auto final_shape = rg.make(final_shape_t, element::i32); OutputVector outputs; for (auto& input : tensors->inputs()) { - outputs.push_back(rg.make(input.get_source_output(), final_shape)); + outputs.push_back(rg.make(input.get_source_output(), final_shape)); } copy_runtime_info_and_name(list_unpack, rg.get(), {input_node}); replace_node(list_unpack, outputs); return true; - } - - if (auto unbind = cast_fw_node(input_node, "aten::unbind")) { + } else if (auto unbind = cast_fw_node(input_node, "aten::unbind")) { const auto input = unbind->get_input_source_output(0); const auto axis = unbind->get_input_source_output(1); const auto num_splits = list_unpack->get_output_size(); - auto split = rg.make(input, axis, num_splits); + auto split = rg.make(input, axis, num_splits); OutputVector outputs; for (auto& output : split->outputs()) { - const auto squeeze = rg.make(output, axis); + const auto squeeze = rg.make(output, axis); outputs.push_back(squeeze); } copy_runtime_info_and_name(list_unpack, rg.get(), {input_node}); replace_node(list_unpack, outputs); return true; - } - if (auto where = cast_fw_node(input_node, "aten::where")) { + } else if (auto where = cast_fw_node(input_node, "aten::where")) { const auto input = where->get_input_source_output(0); - auto non_zero = rg.make(input); - auto axis = opset10::Constant::create(element::i32, Shape{}, {0}); + auto non_zero = rg.make(input); + auto axis = v0::Constant::create(element::i32, Shape{}, {0}); const auto num_splits = list_unpack->get_output_size(); - auto split = rg.make(non_zero, axis, num_splits); + auto split = rg.make(non_zero, axis, num_splits); OutputVector outputs; for (auto& output : split->outputs()) { - const auto squeeze = rg.make(output, axis); + const auto squeeze = rg.make(output, axis); outputs.push_back(squeeze); } copy_runtime_info_and_name(list_unpack, rg.get(), {input_node}); replace_node(list_unpack, outputs); return true; - } - if (auto nonzero_numpy = cast_fw_node(input_node, "aten::nonzero_numpy")) { + } else if (auto nonzero_numpy = cast_fw_node(input_node, "aten::nonzero_numpy")) { const auto input = nonzero_numpy->get_input_source_output(0); - auto non_zero = rg.make(input); - auto axis = opset10::Constant::create(element::i32, Shape{}, {0}); + auto non_zero = rg.make(input); + auto axis = v0::Constant::create(element::i32, Shape{}, {0}); const auto num_splits = list_unpack->get_output_size(); - auto split = rg.make(non_zero, axis, num_splits); + auto split = rg.make(non_zero, axis, num_splits); OutputVector outputs; for (auto& output : split->outputs()) { - const auto squeeze = rg.make(output, axis); + const auto squeeze = rg.make(output, axis); outputs.push_back(squeeze); } copy_runtime_info_and_name(list_unpack, rg.get(), {input_node}); replace_node(list_unpack, outputs); return true; - } - - if (auto meshgrid = cast_fw_node(input_node, "aten::meshgrid")) { + } else if (auto meshgrid = cast_fw_node(input_node, "aten::meshgrid")) { // Input - ListConstruct auto meshgrid_input_node = cast_fw_node(meshgrid->input_value(0).get_node_shared_ptr(), "prim::ListConstruct"); @@ -278,24 +279,24 @@ PrimListUnpackReplacer::PrimListUnpackReplacer() { } NodeVector cat_shapes{}; NodeVector reshapes{}; - auto const_neg_1 = opset10::Constant::create(element::i32, Shape{1}, {-1}); - auto const_1 = opset10::Constant::create(element::i32, Shape{1}, {1}); + auto const_neg_1 = v0::Constant::create(element::i32, Shape{1}, {-1}); + auto const_1 = v0::Constant::create(element::i32, Shape{1}, {1}); int input_idx = 0; for (auto& input : meshgrid_inputs) { - auto reshaped_input = rg.make(input, const_neg_1, false); - auto shape = rg.make(reshaped_input, element::i32); + auto reshaped_input = rg.make(input, const_neg_1, false); + auto shape = rg.make(reshaped_input, element::i32); cat_shapes.push_back(shape); NodeVector cat_inputs(meshgrid_inputs.size(), const_1); cat_inputs[input_idx] = shape; input_idx++; - auto input_cat = rg.make(cat_inputs, 0); - auto reshape_cat = rg.make(reshaped_input, input_cat, false); + auto input_cat = rg.make(cat_inputs, 0); + auto reshape_cat = rg.make(reshaped_input, input_cat, false); reshapes.push_back(reshape_cat); } - auto cat = rg.make(cat_shapes, 0); + auto cat = rg.make(cat_shapes, 0); OutputVector outputs{}; for (auto& reshape : reshapes) { - auto out = rg.make(reshape, cat, ov::op::BroadcastType::BIDIRECTIONAL); + auto out = rg.make(reshape, cat, ov::op::BroadcastType::BIDIRECTIONAL); outputs.push_back(out); } if (indexing == "xy" && outputs.size() >= 2) { @@ -304,17 +305,15 @@ PrimListUnpackReplacer::PrimListUnpackReplacer() { copy_runtime_info_and_name(list_unpack, rg.get(), {input_node, meshgrid_input_node}); replace_node(list_unpack, outputs); return true; - } - - if (auto shape_of = std::dynamic_pointer_cast(input_node)) { + } else if (auto shape_of = std::dynamic_pointer_cast(input_node)) { // case aten::size as input // Number of ListUnpack outputs should be equal to rank of input shape. - auto axis_0 = opset10::Constant::create(element::i32, Shape{}, {0}); - auto split = rg.make(shape_of, axis_0, list_unpack->get_output_size()); + auto axis_0 = v0::Constant::create(element::i32, Shape{}, {0}); + auto split = rg.make(shape_of, axis_0, list_unpack->get_output_size()); OutputVector res; for (auto& output : split->outputs()) { - auto squeeze = rg.make(output, axis_0); + auto squeeze = rg.make(output, axis_0); res.push_back(squeeze); } @@ -322,17 +321,15 @@ PrimListUnpackReplacer::PrimListUnpackReplacer() { replace_node(list_unpack, res); return true; - } - - if (auto slice = std::dynamic_pointer_cast(input_node)) { + } else if (auto slice = std::dynamic_pointer_cast(input_node)) { // case aten::slice as input // Number of ListUnpack outputs should be equal to rank of input shape. - auto axis_0 = opset10::Constant::create(element::i32, Shape{}, {0}); - auto split = rg.make(slice, axis_0, list_unpack->get_output_size()); + auto axis_0 = v0::Constant::create(element::i32, Shape{}, {0}); + auto split = rg.make(slice, axis_0, list_unpack->get_output_size()); OutputVector res; for (auto& output : split->outputs()) { - auto squeeze = rg.make(output, axis_0); + auto squeeze = rg.make(output, axis_0); res.push_back(squeeze); } diff --git a/src/frontends/pytorch/src/utils.cpp b/src/frontends/pytorch/src/utils.cpp index b096da45f25437..df35f11fee1d75 100644 --- a/src/frontends/pytorch/src/utils.cpp +++ b/src/frontends/pytorch/src/utils.cpp @@ -7,8 +7,22 @@ #include "op_table.hpp" #include "openvino/core/rt_info.hpp" #include "openvino/frontend/pytorch/decoder.hpp" +#include "openvino/op/add.hpp" +#include "openvino/op/broadcast.hpp" +#include "openvino/op/concat.hpp" #include "openvino/op/convert_promote_types.hpp" -#include "openvino/opsets/opset10.hpp" +#include "openvino/op/divide.hpp" +#include "openvino/op/gather.hpp" +#include "openvino/op/mod.hpp" +#include "openvino/op/range.hpp" +#include "openvino/op/reduce_prod.hpp" +#include "openvino/op/reshape.hpp" +#include "openvino/op/select.hpp" +#include "openvino/op/shape_of.hpp" +#include "openvino/op/slice.hpp" +#include "openvino/op/squeeze.hpp" +#include "openvino/op/subtract.hpp" +#include "openvino/op/unsqueeze.hpp" #include "openvino/util/log.hpp" #include "pt_framework_node.hpp" #include "translate_session.hpp" @@ -17,6 +31,8 @@ namespace ov { namespace frontend { namespace pytorch { +using namespace ov::op; + void num_inputs_check(const NodeContext& context, size_t min_inputs, size_t max_inputs) { auto num_inputs = context.get_input_size(); FRONT_END_OP_CONVERSION_CHECK(num_inputs >= min_inputs, "Got less inputs than expected"); @@ -38,12 +54,12 @@ Output make_optional_bias(const Output& base_op, if (!context.input_is_none(bias_input_idx)) { auto bias = context.get_input(bias_input_idx); if (!unsqueeze_dims.empty()) { - auto indices = opset10::Constant::create(element::i32, {unsqueeze_dims.size()}, unsqueeze_dims); + auto indices = v0::Constant::create(element::i32, {unsqueeze_dims.size()}, unsqueeze_dims); context.mark_node(indices); - bias = make_shared(bias, indices); + bias = make_shared(bias, indices); context.mark_output(bias); } - return make_shared(context.mark_output(base_op), bias); + return make_shared(context.mark_output(base_op), bias); } else { return base_op; } @@ -52,28 +68,28 @@ Output make_optional_bias(const Output& base_op, Output reshape_channelwise(const NodeContext& context, const Output& data, const Output& shape_source) { - auto input_shape = context.mark_node(std::make_shared(shape_source, element::i32)); - auto input_rank = context.mark_node(std::make_shared(input_shape, element::i32)); - auto one_const = context.mark_node(opset10::Constant::create(element::i32, Shape{1}, {1})); - auto two_const = context.mark_node(opset10::Constant::create(element::i32, Shape{1}, {2})); - auto tail_shape_rank = context.mark_node(std::make_shared(input_rank, two_const)); - auto tail_shape = context.mark_node(std::make_shared(one_const, tail_shape_rank)); - auto channels_dim = context.mark_node(std::make_shared(data, element::i32)); + auto input_shape = context.mark_node(std::make_shared(shape_source, element::i32)); + auto input_rank = context.mark_node(std::make_shared(input_shape, element::i32)); + auto one_const = context.mark_node(v0::Constant::create(element::i32, Shape{1}, {1})); + auto two_const = context.mark_node(v0::Constant::create(element::i32, Shape{1}, {2})); + auto tail_shape_rank = context.mark_node(std::make_shared(input_rank, two_const)); + auto tail_shape = context.mark_node(std::make_shared(one_const, tail_shape_rank)); + auto channels_dim = context.mark_node(std::make_shared(data, element::i32)); auto new_shape = - context.mark_node(std::make_shared(OutputVector{one_const, channels_dim, tail_shape}, 0)); + context.mark_node(std::make_shared(OutputVector{one_const, channels_dim, tail_shape}, 0)); - return context.mark_node(std::make_shared(data, new_shape, false)); + return context.mark_node(std::make_shared(data, new_shape, false)); } std::tuple, Output> get_shape_rank(const NodeContext& context, const Output& x, bool as_scalar, element::Type output_type) { - auto shape = context.mark_node(std::make_shared(x, output_type)); - Output rank = context.mark_node(std::make_shared(shape, output_type)); + auto shape = context.mark_node(std::make_shared(x, output_type)); + Output rank = context.mark_node(std::make_shared(shape, output_type)); if (as_scalar) { - auto axis_0 = context.mark_node(opset10::Constant::create(output_type, Shape{}, {0})); - rank = context.mark_node(std::make_shared(rank, axis_0)); + auto axis_0 = context.mark_node(v0::Constant::create(output_type, Shape{}, {0})); + rank = context.mark_node(std::make_shared(rank, axis_0)); } return std::make_tuple(shape, rank); } @@ -81,24 +97,24 @@ std::tuple, Output> get_shape_rank(const NodeContext& context Output reshape_kernel_for_group(const NodeContext& context, const Output& kernel, int64_t groups) { using std::make_shared; - auto axis_0 = opset10::Constant::create(element::i32, Shape{}, {0}); - auto groups_const = opset10::Constant::create(element::i32, Shape{1}, {groups}); - auto neg_1_const = opset10::Constant::create(element::i32, Shape{1}, {-1}); + auto axis_0 = v0::Constant::create(element::i32, Shape{}, {0}); + auto groups_const = v0::Constant::create(element::i32, Shape{1}, {groups}); + auto neg_1_const = v0::Constant::create(element::i32, Shape{1}, {-1}); - auto kernel_shape = std::make_shared(kernel, element::i32); - auto c_out_idx = opset10::Constant::create(element::i32, Shape{}, {0}); - auto kernel_shape_0 = make_shared(kernel_shape, c_out_idx, axis_0); - auto kernel_shape_0_uns = make_shared(kernel_shape_0, axis_0); - auto c_out_value = make_shared(kernel_shape_0_uns, groups_const); + auto kernel_shape = std::make_shared(kernel, element::i32); + auto c_out_idx = v0::Constant::create(element::i32, Shape{}, {0}); + auto kernel_shape_0 = make_shared(kernel_shape, c_out_idx, axis_0); + auto kernel_shape_0_uns = make_shared(kernel_shape_0, axis_0); + auto c_out_value = make_shared(kernel_shape_0_uns, groups_const); - auto start = opset10::Constant::create(element::i32, Shape{1}, {2}); - auto stop = opset10::Constant::create(element::i32, Shape{1}, {std::numeric_limits::max()}); - auto step = opset10::Constant::create(element::i32, Shape{1}, {1}); - auto remaining_shape = make_shared(kernel_shape, start, stop, step); + auto start = v0::Constant::create(element::i32, Shape{1}, {2}); + auto stop = v0::Constant::create(element::i32, Shape{1}, {std::numeric_limits::max()}); + auto step = v0::Constant::create(element::i32, Shape{1}, {1}); + auto remaining_shape = make_shared(kernel_shape, start, stop, step); auto new_kernel_shape = - make_shared(OutputVector{groups_const, c_out_value, neg_1_const, remaining_shape}, 0); - auto res = make_shared(kernel, new_kernel_shape, false); + make_shared(OutputVector{groups_const, c_out_value, neg_1_const, remaining_shape}, 0); + auto res = make_shared(kernel, new_kernel_shape, false); context.mark_nodes({axis_0, groups_const, kernel_shape, @@ -121,22 +137,22 @@ std::shared_ptr get_axes_range(const NodeContext& context, int input_id) { }; std::shared_ptr get_node_axes_range(const NodeContext& context, const Output& x) { - auto start = std::make_shared(element::i32, Shape{}, 0); - auto step = std::make_shared(element::i32, Shape{}, 1); + auto start = std::make_shared(element::i32, Shape{}, 0); + auto step = std::make_shared(element::i32, Shape{}, 1); Output reduced_rank; std::tie(std::ignore, reduced_rank) = get_shape_rank(context, x, true); - return context.mark_node(std::make_shared(start, reduced_rank, step, element::i32)); + return context.mark_node(std::make_shared(start, reduced_rank, step, element::i32)); }; Output normalize_axis(const NodeContext& context, const Output& axis, const Output& rank) { - auto axis_rank = context.mark_node(std::make_shared(axis, rank)); - return context.mark_node(std::make_shared(axis_rank, rank)); + auto axis_rank = context.mark_node(std::make_shared(axis, rank)); + return context.mark_node(std::make_shared(axis_rank, rank)); } std::shared_ptr numel(const NodeContext& context, const Output& x, element::Type output_type) { - auto input_shape = context.mark_node(std::make_shared(x, output_type)); - auto axes = context.mark_node(opset10::Constant::create(output_type, Shape({1}), {0})); - return context.mark_node(std::make_shared(input_shape, axes, false)); + auto input_shape = context.mark_node(std::make_shared(x, output_type)); + auto axes = context.mark_node(v0::Constant::create(output_type, Shape({1}), {0})); + return context.mark_node(std::make_shared(input_shape, axes, false)); }; namespace { @@ -166,14 +182,14 @@ element::Type convert_dtype(int64_t pt_type) { }; Output apply_dtype(const NodeContext& context, size_t dtype_port, const Output& input_tensor) { - if (std::dynamic_pointer_cast( + if (std::dynamic_pointer_cast( context.get_input_from_visible_context(dtype_port).get_node_shared_ptr())) { auto dtype = convert_dtype(context.const_input(dtype_port)); - return context.mark_node(std::make_shared(input_tensor, dtype)); + return context.mark_node(std::make_shared(input_tensor, dtype)); } else if (const auto& fw_node = cast_fw_node(context.get_input(static_cast(dtype_port)).get_node_shared_ptr(), "prim::dtype")) { auto out_tensor = fw_node->input_value(0); - return context.mark_node(std::make_shared(input_tensor, out_tensor)); + return context.mark_node(std::make_shared(input_tensor, out_tensor)); } else { FRONT_END_OP_CONVERSION_CHECK(false, "Couldn't get dtype input"); } @@ -189,13 +205,13 @@ Output concat_list_construct(const Output& input) { if (auto list_construct = cast_fw_node(input.get_node_shared_ptr(), "prim::ListConstruct")) { auto list_inputs = list_construct->input_values(); OutputVector node_vector; - auto zero = opset10::Constant::create(element::i32, Shape{}, {0}); + auto zero = v0::Constant::create(element::i32, Shape{}, {0}); for (size_t i = 0; i < list_inputs.size(); i++) { auto node = concat_list_construct(list_inputs[i]); - auto unsqueezed_node = std::make_shared(node, zero); + auto unsqueezed_node = std::make_shared(node, zero); node_vector.push_back(unsqueezed_node); } - return std::make_shared(node_vector, 0); + return std::make_shared(node_vector, 0); } return input; } @@ -409,11 +425,11 @@ void align_eltwise_input_types(const NodeContext& context, // If only one input is PyScalar, replace it with const to mitigate issues with dynamic type caused by dynamic // shape. if (is_lhs_python_scalar && !is_rhs_python_scalar) { - tmp_lhs = context.mark_node(std::make_shared(const_0, lhs)); - tmp_rhs = context.mark_node(std::make_shared(const_1, rhs)); + tmp_lhs = context.mark_node(std::make_shared(const_0, lhs)); + tmp_rhs = context.mark_node(std::make_shared(const_1, rhs)); } else if (!is_lhs_python_scalar && is_rhs_python_scalar) { - tmp_lhs = context.mark_node(std::make_shared(const_1, lhs)); - tmp_rhs = context.mark_node(std::make_shared(const_0, rhs)); + tmp_lhs = context.mark_node(std::make_shared(const_1, lhs)); + tmp_rhs = context.mark_node(std::make_shared(const_0, rhs)); } auto at = context.mark_node( @@ -422,15 +438,15 @@ void align_eltwise_input_types(const NodeContext& context, if (dst_type.is_dynamic()) { // Add ConvertLike on original node to not remove changes to shape done to differentiate between tensors and // scalars. - lhs = context.mark_node(std::make_shared(lhs, at->output(0))); - rhs = context.mark_node(std::make_shared(rhs, at->output(1))); + lhs = context.mark_node(std::make_shared(lhs, at->output(0))); + rhs = context.mark_node(std::make_shared(rhs, at->output(1))); } else { // Cast to destination type if (dst_type != lhs_type) { - lhs = context.mark_node(std::make_shared(lhs, dst_type)); + lhs = context.mark_node(std::make_shared(lhs, dst_type)); } if (dst_type != rhs_type) { - rhs = context.mark_node(std::make_shared(rhs, dst_type)); + rhs = context.mark_node(std::make_shared(rhs, dst_type)); } } return; @@ -442,7 +458,7 @@ void align_output_types(const NodeContext& context, OutputVector& outputs) { if (dtype_any.is()) { auto dtype = dtype_any.as(); if (dtype.is_static() && dtype != outputs[i].get_element_type()) { - outputs[i] = std::make_shared(outputs[i], dtype); + outputs[i] = std::make_shared(outputs[i], dtype); } } } @@ -547,9 +563,9 @@ Output masked_fill(ov::pass::NodeRegistry& rg, const Output& data, const Output& mask, const Output& value) { - auto _value = rg.make(value, data); - auto bool_mask = rg.make(mask, element::boolean); - return rg.make(bool_mask, _value, data); + auto _value = rg.make(value, data); + auto bool_mask = rg.make(mask, element::boolean); + return rg.make(bool_mask, _value, data); } Output concat_list_from_inputs(const NodeContext& context, size_t begin, size_t end) { From c85902d9b5b2f73b44c08b14e2133c201b7a91cc Mon Sep 17 00:00:00 2001 From: Egor Duplenskii Date: Thu, 22 Aug 2024 23:47:20 +0200 Subject: [PATCH 26/35] [INSTALL] Fix setupvars.sh corrupted installation directory (#26166) Both 'cd' and 'pwd -P' print the directory name Installation directory should be printed only once --- scripts/setupvars/setupvars.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/setupvars/setupvars.sh b/scripts/setupvars/setupvars.sh index 54eefc25714400..dd67bf330630c1 100755 --- a/scripts/setupvars/setupvars.sh +++ b/scripts/setupvars/setupvars.sh @@ -6,7 +6,7 @@ abs_path () { script_path=$(eval echo "$1") directory=$(dirname "$script_path") - builtin cd "$directory" || exit + builtin cd "$directory" >/dev/null 2>&1 || exit pwd -P } From eb16f7fbd0d450d51894e07ef1888c71505b4b0a Mon Sep 17 00:00:00 2001 From: Taylor Yeonbok Lee Date: Thu, 22 Aug 2024 14:47:54 -0700 Subject: [PATCH 27/35] [GPU] Optimize fc_bf_tiled kernel for large K + small N case (#26054) ### Details: - Optimize fc_bf_tiled kernel for large K + small N case by setting K_TILE_SIZE 4 - Perf gain on MTL (U7 155H + 32GB RAM + driver 31.0.101.5333) ![image](https://github.com/user-attachments/assets/2e6537fe-90d4-47e1-8d56-f816d3471559) (No regression and on par on llama3 INT4 default and phi-3 mini INT4 default) ### Tickets: - 149212 --- .../fully_connected_gpu_bf_tiled.cl | 23 ++++++-- .../include/batch_headers/fetch_weights.cl | 10 ++++ .../include/batch_headers/int4_utils.cl | 13 +++++ .../cl_kernels/reorder_weights_int4.cl | 24 ++++++++ .../fully_connected_kernel_bf_tiled.cpp | 55 ++++++++++++++++--- .../kernels/reorder/reorder_weights_int4.cpp | 5 ++ 6 files changed, 115 insertions(+), 15 deletions(-) diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/fully_connected_gpu_bf_tiled.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/fully_connected_gpu_bf_tiled.cl index 0366f602d69982..92be3f31f97f3f 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/fully_connected_gpu_bf_tiled.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/fully_connected_gpu_bf_tiled.cl @@ -108,7 +108,7 @@ KERNEL(quantize_input)( #define OUTPUT_BLOCK_WRITE(ptr, offset, val) BLOCK_WRITEN(OUTPUT_TYPE, TILE_OFM, ptr, offset, val) #define SLM_FILTER_VEC MAKE_VECTOR_TYPE(ACCUMULATOR_TYPE, TILE_OFM) -#define SLM_FILTER_PACKED_VEC MAKE_VECTOR_TYPE(FILTER_TYPE, FILTER_LOAD_BLOCK_SIZE) +#define SLM_FILTER_PACKED_VEC MAKE_VECTOR_TYPE(FILTER_TYPE, FILTER_ACTUAL_LOAD_BLOCK_SIZE) #define SLM_FILTER_UNPACKED_VEC MAKE_VECTOR_TYPE(ACCUMULATOR_TYPE, FILTER_ELEMENTS_PER_LOAD) @@ -311,6 +311,9 @@ inline void FUNC(fc_bf_tiled_kernel_default)( #if TILE_OFM != 2 #error "FC bf_tiled kernel: can't use SLM optimization with TILE_OFM != 2" #endif + #if FILTER_LAYOUT_OS_IYX_OSV16 && TILE_K != 4 + #error "FC bf_tiled kernel: can't use SLM optimization with TILE_K != 2 && OS_IYX_OSV16 layout" + #endif // Skip first barrier synchronization if there is only single outer loop iteration. #if MAIN_LOOP_ELEMENTS_COUNT / (TILE_IFM * SIMD) > 1 @@ -319,12 +322,19 @@ inline void FUNC(fc_bf_tiled_kernel_default)( __local SLM_FILTER_VEC* slm_wei_vec = (__local SLM_FILTER_VEC*)wei_local_mem; - uint weights_idx = weights_offset + local_id * SIMD * FILTER_LOAD_ITERS * FILTER_LOAD_BLOCK_SIZE; + uint weights_idx = weights_offset + local_id * SIMD * FILTER_LOAD_ITERS * FILTER_ACTUAL_LOAD_BLOCK_SIZE; uint wei_local_idx = local_id * SIMD * FILTER_LOAD_ITERS * FILTER_LOAD_BLOCK_SIZE + sglid; - unroll_for(uint load_iter = 0; load_iter < FILTER_LOAD_ITERS; ++load_iter) { - SLM_FILTER_PACKED_VEC wei_packed = BLOCK_READN(FILTER_TYPE, FILTER_LOAD_BLOCK_SIZE, weights, weights_idx); + #if FILTER_LAYOUT_OS_IYX_OSV16 + SLM_FILTER_PACKED_VEC wei_packed0 = BLOCK_READN(FILTER_TYPE, FILTER_ACTUAL_LOAD_BLOCK_SIZE, weights, weights_idx); + SLM_FILTER_PACKED_VEC wei_packed1 = BLOCK_READN(FILTER_TYPE, FILTER_ACTUAL_LOAD_BLOCK_SIZE, weights, (weights_idx + ((IFM_SIZE / 2) * 16))); + SLM_FILTER_UNPACKED_VEC wei_unpacked; + wei_unpacked.s0123 = UNPACK_INT4(ACCUMULATOR_TYPE, *((INT4_PACKED_TYPE_PRELOAD*)&wei_packed0)); + wei_unpacked.s4567 = UNPACK_INT4(ACCUMULATOR_TYPE, *((INT4_PACKED_TYPE_PRELOAD*)&wei_packed1)); + #else + SLM_FILTER_PACKED_VEC wei_packed = BLOCK_READN(FILTER_TYPE, FILTER_LOAD_BLOCK_SIZE/*4*/, weights, weights_idx); SLM_FILTER_UNPACKED_VEC wei_unpacked = UNPACK_INT4(ACCUMULATOR_TYPE, *((INT4_PACKED_TYPE_PRELOAD*)&wei_packed)); + #endif ACCUMULATOR_TYPE* w = (ACCUMULATOR_TYPE*)(&wei_unpacked); unroll_for(uint fi = 0; fi < TILE_OFM; ++fi) { unroll_for(uint kii = 0; kii < FILTER_LOAD_BLOCK_SIZE; ++kii) { @@ -383,8 +393,7 @@ inline void FUNC(fc_bf_tiled_kernel_default)( #endif #undef STORE_TO_SLM - - weights_idx += SIMD * FILTER_LOAD_BLOCK_SIZE; + weights_idx += SIMD * FILTER_ACTUAL_LOAD_BLOCK_SIZE; } wei_local_idx = sglid; @@ -478,6 +487,8 @@ inline void FUNC(fc_bf_tiled_kernel_default)( } #if TILE_OFM == 1 && FILTER_LAYOUT_OS_IS_YX_OSV32_ISV2 weights_offset += TILE_K_OFM_PACKED * 2 * SIMD; + #elif FILTER_LAYOUT_OS_IYX_OSV16 && TILE_OFM == 2 && USE_SLM == 1 + weights_offset += TILE_K_OFM_PACKED / 2 * SIMD; #else weights_offset += TILE_K_OFM_PACKED * SIMD; #endif diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/include/batch_headers/fetch_weights.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/include/batch_headers/fetch_weights.cl index 531bd0838a3aa0..7135673c8c5dc8 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/include/batch_headers/fetch_weights.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/include/batch_headers/fetch_weights.cl @@ -405,6 +405,16 @@ inline uint get_os_zyxi_osv16_index(uint o, uint i, uint z, uint y, uint x, uint ((o) / (sub_group_size))*CAT(prefix, _OFM_PITCH) \ ) +#define GET_FILTER_OS_IYX_OSV_INDEX_INT4_PACKED(prefix, o, i, y, x, sub_group_size) \ + CAT(prefix, _OFFSET) + \ + ((o) % (sub_group_size)) + \ + (sub_group_size)*( \ + (x)*CAT(prefix, _X_PITCH) + \ + (y)*CAT(prefix, _Y_PITCH) + \ + (i)*CAT(prefix, _IFM_PITCH) + \ + ((o) / (sub_group_size))*(CAT(prefix, _OFM_PITCH)/2) \ + ) + #define GET_FILTER_OS_IS_YX_OSV_ISV_INDEX_INT4_PACKED(prefix, o, i, y, x, sub_group_size) \ CAT(prefix, _OFFSET) + \ ((o) % (sub_group_size)) + \ diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/include/batch_headers/int4_utils.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/include/batch_headers/int4_utils.cl index fb257169a8b78c..d919d1ce1104ab 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/include/batch_headers/int4_utils.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/include/batch_headers/int4_utils.cl @@ -143,12 +143,24 @@ inline half4 unpack_to_half(uint4x4_t v) __attribute__((overloadable)) { return (half4)(f0.s0, f0.s1, f1.s0, f1.s1); } +inline half4 unpack_to_half_osv32_isv2(uint4x4_t v) __attribute__((overloadable)) { + half2 f0 = unpack_to_half(v.s0); + half2 f1 = unpack_to_half(v.s1); + return (half4)(f0.s0, f0.s1, f1.s0, f1.s1); +} + inline half4 unpack_to_half(int4x4_t v) __attribute__((overloadable)) { half2 f0 = unpack_to_half(v.s0); half2 f1 = unpack_to_half(v.s1); return (half4)(f0.s0, f0.s1, f1.s0, f1.s1); } +inline half4 unpack_to_half_osv32_isv2(int4x4_t v) __attribute__((overloadable)) { + half2 f0 = unpack_to_half(v.s0); + half2 f1 = unpack_to_half(v.s1); + return (half4)(f0.s0, f0.s1, f1.s0, f1.s1); +} + inline half8 unpack_to_half(uint4x8_t v) __attribute__((overloadable)) { half2 f0 = unpack_to_half(v.s0); half2 f1 = unpack_to_half(v.s1); @@ -211,4 +223,5 @@ inline uchar8 unpack_to_uchar_osv32_isv2(uint4x8_t v) __attribute__((overloadabl #define UNPACK_INT4x2(target_type, value) CAT(unpack_to_, target_type)(value) #define UNPACK_INT4x2_OSV32_ISV2(target_type, value) CAT(CAT(unpack_to_, target_type), _osv32_isv2)(value) +#define UNPACK_INT4x4_OSV32_ISV2(target_type, value) CAT(CAT(unpack_to_, target_type), _osv32_isv2)(value) #define UNPACK_TRANSPOSED_INT4x2(target_type, value) CAT(unpack_transposed_to_, target_type)(value) diff --git a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_weights_int4.cl b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_weights_int4.cl index 1b3239d082ec2a..90a7e8bd8feecc 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_weights_int4.cl +++ b/src/plugins/intel_gpu/src/kernel_selector/cl_kernels/reorder_weights_int4.cl @@ -28,6 +28,30 @@ KERNEL(reorder_weights_int4)(const __global INPUT0_TYPE* input, __global OUTPUT_ OUTPUT_TYPE out = in0 | (in1 << 4); output[out_byte_offset] = out; +#elif defined(OUTPUT_LAYOUT_OS_IYX_OSV16) + // osv32_isv2 layout for int4 packed weight + // f0_k0k1 | f1_k0k1 | .... | f15_k0k1 + // f0_k2k3 | f1_k2k3 | .... | f15_k2k3 + // f0_k3k4 | f1_k3k4 | .... | f15_k3k4 + // ... + // f0_k(K/2-2)k(K/2-1) | f1_k(K/2-2)k(K/2-1) | ....f15_k(K/2-2)k(K/2-1) + // ------------------------------------- + // f16_k2k3 | f17_k2k3 | ... | f31_k2k3 + // ... + const unsigned o = (uint)get_global_id(0); + const unsigned i = (uint)get_global_id(1) * 2; + + const uint input0_offset = GET_FILTER_INDEX(INPUT0, 0, o, i, 0, 0); + + INPUT0_TYPE in1 = input[input0_offset / 2] & 0xFF; + + INPUT0_TYPE packed_out_channels = in1; + + const uint output_idx = GET_FILTER_OS_IYX_OSV_INDEX_INT4_PACKED(OUTPUT, o, i/2, 0, 0, 16); // Calculate offset as osv16 due to packing + output[output_idx] = packed_out_channels; + + + #elif defined(OUTPUT_LAYOUT_OS_IYX_OSV32) // os_iyx osv32 layout for int4 packed weight // k0_f0f16 | k0_f1f17 | .... | k0_f15f31 || k1_f0f16 | k1_f1f17 | ... | k1_f15f31 diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/fully_connected/fully_connected_kernel_bf_tiled.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/fully_connected/fully_connected_kernel_bf_tiled.cpp index 61919c10b816ff..db42bc969b4a32 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/fully_connected/fully_connected_kernel_bf_tiled.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/fully_connected/fully_connected_kernel_bf_tiled.cpp @@ -97,6 +97,19 @@ static bool should_dynamic_quantize(const fully_connected_params& params) { return false; } +static bool is_weight_with_small_ofm(const fully_connected_params& params, size_t output_f) { + size_t min_num_threads = params.engineInfo.computeUnitsCount * simd; + GPU_DEBUG_TRACE_DETAIL << "out_ofm (== weight N dim) size " << output_f << " is small compared to the available threads. " + << "(computeUnitsCount : " << params.engineInfo.computeUnitsCount + << " min_num_threads : " << min_num_threads << ")" << std::endl; + GPU_DEBUG_TRACE_DETAIL << "Use ofm_tile size 1 if the batch size is 1." << std::endl; + return (output_f / 2 /*most frequently used tile_ofm*/ <= min_num_threads); +} + +static bool is_weight_with_large_ifm(const fully_connected_params& fc_params) { + return (fc_params.weights.IFM().v >= fc_params.weights.OFM().v * 3 && fc_params.weights.OFM().v <= 4096); +} + FullyConnected_bf_tiled::FullyConnected_bf_tiled() : FullyConnectedKernelBase("fully_connected_gpu_bf_tiled") { for (unsigned tile_b = 1; tile_b <= 32; ++tile_b) for (unsigned tile_ofm = 1; tile_ofm <= 4; tile_ofm *= 2) @@ -324,14 +337,18 @@ FullyConnected_bf_tiled::GetAutoTuneParams(const fully_connected_params& params, if (params.weights.GetDType() == WeightsType::UINT4 || params.weights.GetDType() == WeightsType::INT4) { if (!params.is_shape_agnostic && batch == 1) { // Tuning for Meteor Lake - size_t min_num_threads = params.engineInfo.computeUnitsCount * simd; - if (output_f / 2 <= min_num_threads && params.weights.GetLayout() == WeightsLayout::os_is_yx_osv32_isv2) { - GPU_DEBUG_TRACE_DETAIL << "FC bf tiled: Set ofm_tile 1. (output_f : " << output_f - << ", computeUnitsCount : " << params.engineInfo.computeUnitsCount - << " min_num_threads : " << min_num_threads << ")" << std::endl; - return selector.Default(tune_params(1, 1, 4, 2, 1, 1, EXE_MODE_DEFAULT)); + if (is_weight_with_small_ofm(params, output_f)) { + if (params.weights.GetLayout() == WeightsLayout::os_is_yx_osv32_isv2) { + return selector.Default(tune_params(1, 1, 4, 2, 1, 1, EXE_MODE_DEFAULT)); + } else if (params.weights.GetLayout() == WeightsLayout::os_iyx_osv16) { + return selector.Default(tune_params(1, 1, 4, 4, 1, 1, EXE_MODE_DEFAULT)); + } } else { - return selector.Default(tune_params(1, 2, 4, 2, 1, 1, EXE_MODE_DEFAULT)); + if (params.weights.GetLayout() == WeightsLayout::os_iyx_osv16) { + return selector.Default(tune_params(1, 1, 4, 4, 1, 1, EXE_MODE_DEFAULT)); + } else { + return selector.Default(tune_params(1, 2, 4, 2, 1, 1, EXE_MODE_DEFAULT)); + } } } else { // Try to use SLM kernels if possible @@ -343,7 +360,10 @@ FullyConnected_bf_tiled::GetAutoTuneParams(const fully_connected_params& params, selector.Case(tune_params(8, 2, 2, 4, 1, 1, EXE_MODE_DEFAULT, KernelType::SLM)) .Case(tune_params(8, 2, 1, 4, 1, 1, EXE_MODE_DEFAULT, KernelType::SLM)); } - return selector.Default(tune_params(8, 2, 1, 4, 1, 1, EXE_MODE_DEFAULT)); + if (params.weights.GetLayout() == WeightsLayout::os_iyx_osv16) + return selector.Default(tune_params(8, 1, 1, 4, 1, 1, EXE_MODE_DEFAULT)); + else + return selector.Default(tune_params(8, 2, 1, 4, 1, 1, EXE_MODE_DEFAULT)); } } else if (params.compressed && params.engineInfo.supports_immad) { return selector.Default(tune_params(1, 1, 1, 4, 1, 1, EXE_MODE_DEFAULT)); @@ -480,6 +500,8 @@ JitConstants FullyConnected_bf_tiled::GetJitConstants(const fully_connected_para } if (params.weights.GetLayout() == WeightsLayout::os_is_yx_osv32_isv2) jit.AddConstant(MakeJitConstant("W_IDX", "fi * TILE_K + kii")); + else if (params.weights.GetLayout() == WeightsLayout::os_iyx_osv16) + jit.AddConstant(MakeJitConstant("W_IDX", "fi * TILE_K + kii")); else jit.AddConstant(MakeJitConstant("W_IDX", "kii * TILE_OFM + fi")); @@ -512,9 +534,17 @@ JitConstants FullyConnected_bf_tiled::GetJitConstants(const fully_connected_para jit.AddConstant(MakeJitConstant("USE_SLM", 1)); jit.AddConstant(MakeJitConstant("LWS_BATCHES", lws_batches)); jit.AddConstant(MakeJitConstant("FILTER_LOAD_ITERS", weights_load_iters)); + + if (params.weights.GetLayout() == WeightsLayout::os_iyx_osv16) { + jit.AddConstant(MakeJitConstant("FILTER_ACTUAL_LOAD_BLOCK_SIZE", block_read_size / 2)); + jit.Merge(make_int4_packed_type_jit_constant("INT4_PACKED_TYPE_PRELOAD", params.weights.GetDType(), weights_elements_per_load / 2)); + } else { + jit.AddConstant(MakeJitConstant("FILTER_ACTUAL_LOAD_BLOCK_SIZE", block_read_size)); + jit.Merge(make_int4_packed_type_jit_constant("INT4_PACKED_TYPE_PRELOAD", params.weights.GetDType(), weights_elements_per_load)); + } + jit.AddConstant(MakeJitConstant("FILTER_LOAD_BLOCK_SIZE", block_read_size)); jit.AddConstant(MakeJitConstant("FILTER_ELEMENTS_PER_LOAD", weights_elements_per_load)); - jit.Merge(make_int4_packed_type_jit_constant("INT4_PACKED_TYPE_PRELOAD", params.weights.GetDType(), weights_elements_per_load)); } else { jit.AddConstant(MakeJitConstant("USE_SLM", 0)); } @@ -675,9 +705,16 @@ KernelsData FullyConnected_bf_tiled::GetTunedKernelsDataByIndex(const Params &pa return {}; tune_params tparams = GetAutoTuneParams(fc_params, KernelType::ANY, autoTuneIndex); + auto output_f = get_output_aligned_bf_size(fc_params, false).second; WeightsLayout weights_layout = WeightsLayout::os_iyx_osv16; if (fc_params.compressed && fc_params.inputs[0].GetDType() == Datatype::F16 + && (fc_params.weights.GetDType() == WeightsType::INT4 || fc_params.weights.GetDType() == WeightsType::UINT4) + && is_weight_with_small_ofm(fc_params, output_f) && is_weight_with_large_ifm(fc_params) + && (fc_params.weights.GetLayout() == WeightsLayout::oiyx || fc_params.weights.GetLayout() == WeightsLayout::os_iyx_osv16)) { + // Large K + Small N case to use [osv16 + TILE_K 4] + TILE_OFM 1 for batch 1 + weights_layout = WeightsLayout::os_iyx_osv16; + } else if (fc_params.compressed && fc_params.inputs[0].GetDType() == Datatype::F16 // ioyx => os_is_yx_osv32_isv2 is not supported yet && (fc_params.weights.GetLayout() == WeightsLayout::oiyx || fc_params.weights.GetLayout() == WeightsLayout::os_is_yx_osv32_isv2) && (fc_params.weights.GetDType() == WeightsType::INT4 || fc_params.weights.GetDType() == WeightsType::UINT4)) { diff --git a/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_weights_int4.cpp b/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_weights_int4.cpp index 0a8a2e168fe3bd..cd55dacd4c9c7a 100644 --- a/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_weights_int4.cpp +++ b/src/plugins/intel_gpu/src/kernel_selector/kernels/reorder/reorder_weights_int4.cpp @@ -6,6 +6,7 @@ #include "kernel_selector_common.h" #include "kernel_selector_params.h" #include "kernel_selector_utils.h" +#include "common_types.h" namespace kernel_selector { @@ -17,6 +18,7 @@ ParamsKey ReorderWeightsKernelInt4::GetSupportedKey() const { k.EnableOutputWeightsType(WeightsType::INT4); k.EnableInputWeightsLayout(WeightsLayout::oiyx); k.EnableInputWeightsLayout(WeightsLayout::ioyx); + k.EnableOutputWeightsLayout(WeightsLayout::os_iyx_osv16); k.EnableOutputWeightsLayout(WeightsLayout::os_iyx_osv32); k.EnableOutputWeightsLayout(WeightsLayout::os_is_yx_osv32_isv2); k.EnableOutputWeightsLayout(WeightsLayout::oiyx); @@ -40,6 +42,8 @@ ReorderWeightsKernelInt4::DispatchData ReorderWeightsKernelInt4::SetDefault(cons dispatchData.gws = { Align(output.OFM().v, 32) / 2, output.IFM().v, 1 }; } else if (output.GetLayout() == WeightsLayout::os_is_yx_osv32_isv2) { dispatchData.gws = { Align(output.OFM().v, 32), output.IFM().v / 2, 1 }; + } else if (output.GetLayout() == WeightsLayout::os_iyx_osv16) { + dispatchData.gws = { Align(output.OFM().v, 16), output.IFM().v / 2, 1 }; } else { dispatchData.gws = { CeilDiv(output.LogicalSize(), 2), 1, 1 }; } @@ -60,6 +64,7 @@ bool ReorderWeightsKernelInt4::Validate(const Params& params) const { bool supported_case = input.GetLayout() == WeightsLayout::oiyx && output.GetLayout() == WeightsLayout::os_iyx_osv32; supported_case |= input.GetLayout() == WeightsLayout::oiyx && output.GetLayout() == WeightsLayout::os_is_yx_osv32_isv2; + supported_case |= input.GetLayout() == WeightsLayout::oiyx && output.GetLayout() == WeightsLayout::os_iyx_osv16; supported_case |= input.GetLayout() == WeightsLayout::ioyx && output.GetLayout() == WeightsLayout::oiyx; supported_case |= input.GetLayout() == WeightsLayout::ioyx && output.GetLayout() == WeightsLayout::os_iyx_osv32; return supported_case; From f6d4b4014e00c2513a1143eb3253e9890c97c6e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Aug 2024 04:47:53 +0000 Subject: [PATCH 28/35] Bump idna from 3.4 to 3.7 in /docs (#26189) Bumps [idna](https://github.com/kjd/idna) from 3.4 to 3.7.

Release notes

Sourced from idna's releases.

v3.7

What's Changed

  • Fix issue where specially crafted inputs to encode() could take exceptionally long amount of time to process. [CVE-2024-3651]

Thanks to Guido Vranken for reporting the issue.

Full Changelog: https://github.com/kjd/idna/compare/v3.6...v3.7

Changelog

Sourced from idna's changelog.

3.7 (2024-04-11) ++++++++++++++++

  • Fix issue where specially crafted inputs to encode() could take exceptionally long amount of time to process. [CVE-2024-3651]

Thanks to Guido Vranken for reporting the issue.

3.6 (2023-11-25) ++++++++++++++++

  • Fix regression to include tests in source distribution.

3.5 (2023-11-24) ++++++++++++++++

  • Update to Unicode 15.1.0
  • String codec name is now "idna2008" as overriding the system codec "idna" was not working.
  • Fix typing error for codec encoding
  • "setup.cfg" has been added for this release due to some downstream lack of adherence to PEP 517. Should be removed in a future release so please prepare accordingly.
  • Removed reliance on a symlink for the "idna-data" tool to comport with PEP 517 and the Python Packaging User Guide for sdist archives.
  • Added security reporting protocol for project

Thanks Jon Ribbens, Diogo Teles Sant'Anna, Wu Tingfeng for contributions to this release.

Commits
  • 1d365e1 Release v3.7
  • c1b3154 Merge pull request #172 from kjd/optimize-contextj
  • 0394ec7 Merge branch 'master' into optimize-contextj
  • cd58a23 Merge pull request #152 from elliotwutingfeng/dev
  • 5beb28b More efficient resolution of joiner contexts
  • 1b12148 Update ossf/scorecard-action to v2.3.1
  • d516b87 Update Github actions/checkout to v4
  • c095c75 Merge branch 'master' into dev
  • 60a0a4c Fix typo in GitHub Actions workflow key
  • 5918a0e Merge branch 'master' into dev
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=idna&package-manager=pip&previous-version=3.4&new-version=3.7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/openvinotoolkit/openvino/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index a3873d9e77c27e..84813255ac7694 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -8,7 +8,7 @@ certifi==2024.7.4 colorama==0.4.6 Cython==0.29.33 docutils==0.20 -idna==3.4 +idna==3.7 imagesize==1.3.0 importlib-metadata==4.8.0 iniconfig==1.1.1 From e4bcb7a8385d0902747450f54ed82902abc01fef Mon Sep 17 00:00:00 2001 From: hyunback kim Date: Fri, 23 Aug 2024 14:43:05 +0900 Subject: [PATCH 29/35] [GPU] Fix to get bad groups size bug in compressed_fc static shape. (#26145) SD3.int4 static bad accuracy problem, fixed it. ### Tickets: - *146778* --------- Signed-off-by: hyunback --- .../graph_optimizer/prepare_quantization.cpp | 3 +- .../test_cases/fully_connected_gpu_test.cpp | 90 +++++++++++++++++++ 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/src/plugins/intel_gpu/src/graph/graph_optimizer/prepare_quantization.cpp b/src/plugins/intel_gpu/src/graph/graph_optimizer/prepare_quantization.cpp index c17490488b629e..9563a1531cb886 100644 --- a/src/plugins/intel_gpu/src/graph/graph_optimizer/prepare_quantization.cpp +++ b/src/plugins/intel_gpu/src/graph/graph_optimizer/prepare_quantization.cpp @@ -511,8 +511,7 @@ static void optimize_weights_decompression_parameters(fully_connected_node& fc_n auto need_reorder = [&](size_t dep_id) { auto dep_layout = fc_node.get_input_layout(dep_id); auto dep_pshape = dep_layout.get_partial_shape(); - - auto groups_count = dep_pshape[dep_pshape.size() - 1].get_length(); + auto groups_count = dep_pshape[1].get_length(); return groups_count > 1; }; diff --git a/src/plugins/intel_gpu/tests/unit/test_cases/fully_connected_gpu_test.cpp b/src/plugins/intel_gpu/tests/unit/test_cases/fully_connected_gpu_test.cpp index 3f0b43a861873b..7a9519ba48c05a 100644 --- a/src/plugins/intel_gpu/tests/unit/test_cases/fully_connected_gpu_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/test_cases/fully_connected_gpu_test.cpp @@ -3451,6 +3451,96 @@ TEST(fully_connected_3d_onednn_gpu, no_biases_int8) { ASSERT_EQ(-52.0f, output_ptr[b * output_f + 3]); } } + +TEST(fully_connected_3d_onednn_gpu, compressed_int4_scale_static) { + tests::random_generator rg(GET_SUITE_NAME); + + auto& engine = get_test_engine(); + if (!engine.get_device_info().supports_immad) + return; + + long int batch_num = 2; + long int ifm_num = 64; + long int ofm_num = 4; + long int scales_group_size = 32; + + auto input_mem = engine.allocate_memory({ { 1, batch_num, ifm_num, 1}, data_types::f16, format::bfyx }); + auto weights_mem = engine.allocate_memory({ {ofm_num, ifm_num, 1, 1}, data_types::u4, format::bfyx }); + auto scale_mem = engine.allocate_memory({ {ofm_num, ifm_num / scales_group_size, 1, 1}, data_types::f16, format::bfyx }); + auto dcomp_zp_mem = engine.allocate_memory({ {1, 1, 1, 1}, data_types::u8, format::bfyx }); + + set_values(dcomp_zp_mem, {8}); + + auto input_data = rg.generate_random_1d(batch_num * ifm_num, -2.0f, 2.0f); + set_values(input_mem, input_data); + + auto weigths_data = rg.generate_random_1d(ofm_num * ifm_num / 2, 0, 10); + set_values(weights_mem, weigths_data); + + auto scale_data = rg.generate_random_1d(ofm_num * ifm_num / scales_group_size, -4.0f, 4.0f); + set_values(scale_mem, scale_data); + + auto in_layout = layout{ {1, batch_num, ifm_num, 1}, data_types::f16, format::bfyx }; + + auto fc_prim = fully_connected("fc_prim", input_info("input"), "weights", "", "scale", "dcomp_zp", data_types::f16, 3, 4); + + fc_prim.decompression_zero_point_scalar = 8; + + auto get_ref_results = [&]() { + topology topology( + input_layout("input", in_layout), + data("weights", weights_mem), + data("scale", scale_mem), + data("dcomp_zp", dcomp_zp_mem), + fc_prim + ); + + auto config = get_test_default_config(engine); + ov::intel_gpu::ImplementationDesc fc_impl_desc = { format::bfyx, "fully_connected_gpu_bfyx_ref", impl_types::ocl }; + config.set_property(ov::intel_gpu::force_implementations(ov::intel_gpu::ImplForcingMap{ {"fc_prim", fc_impl_desc} })); + + network network(engine, topology, config); + network.set_input_data("input", input_mem); + + auto outputs = network.execute(); + OPENVINO_ASSERT(outputs.size() == 1); + OPENVINO_ASSERT(outputs.begin()->first == "fc_prim"); + + auto output_layout = outputs.begin()->second.get_layout(); + auto output_mem = outputs.begin()->second.get_memory(); + + return engine.reinterpret_buffer(*output_mem, output_layout); + }; + + topology topology( + input_layout("input", in_layout), + data("weights", weights_mem), + data("scale", scale_mem), + data("dcomp_zp", dcomp_zp_mem), + fc_prim + ); + + auto config = get_test_default_config(engine); + config.set_property(ov::intel_gpu::optimize_data(true)); + + network::ptr network = get_network(engine, topology, config, get_test_stream_ptr(), false); + + network->set_input_data("input", input_mem); + + auto outputs = network->execute(); + ASSERT_EQ(outputs.size(), size_t(1)); + ASSERT_EQ(outputs.begin()->first, "fc_prim"); + + auto output_mem = outputs.begin()->second.get_memory(); + cldnn::mem_lock output_ptr (output_mem, get_test_stream()); + + auto ref_output_mem = get_ref_results(); + cldnn::mem_lock output_ptr_ref (ref_output_mem, get_test_stream()); + + for (size_t i = 0; i < output_ptr_ref.size(); i++) { + ASSERT_NEAR(output_ptr_ref[i], output_ptr[i], 9.0) << "i = " << i << std::endl; + } +} #endif TEST_F(fully_connected_gpu_tests, compressed_scale_zp_bias) { From 1f3edda195cba4f46d3cbbf0d9f0f5d867e80a6b Mon Sep 17 00:00:00 2001 From: Mateusz Mikolajczyk Date: Fri, 23 Aug 2024 08:08:42 +0200 Subject: [PATCH 30/35] [Transformations] Add downgrade transformation for ScatterNDUpdate15 with Reduction::None (#26113) ### Details: - *Add downgrade transformation from ScatterNDUpdate15 with Reduction::None to ScatterNDUpdate3* - *...* ### Tickets: - *CVS-149830* --------- Co-authored-by: Roman Kazantsev --- .../convert_scatter_nd_update15_downgrade.hpp | 24 +++++++++ .../common_optimizations.cpp | 2 + .../common_optimizations/nop_elimination.cpp | 6 ++- .../convert_scatter_nd_update15_downgrade.cpp | 39 ++++++++++++++ .../common_optimizations/nop_elimination.cpp | 22 ++++++++ ...ert_scatter_nd_update15_downgrade_test.cpp | 54 +++++++++++++++++++ .../transformation_pipeline.cpp | 2 + .../test_tf_SparseTensorDenseMatMul.py | 2 - 8 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 src/common/transformations/include/transformations/op_conversions/convert_scatter_nd_update15_downgrade.hpp create mode 100644 src/common/transformations/src/transformations/op_conversions/convert_scatter_nd_update15_downgrade.cpp create mode 100644 src/common/transformations/tests/op_conversions/convert_scatter_nd_update15_downgrade_test.cpp diff --git a/src/common/transformations/include/transformations/op_conversions/convert_scatter_nd_update15_downgrade.hpp b/src/common/transformations/include/transformations/op_conversions/convert_scatter_nd_update15_downgrade.hpp new file mode 100644 index 00000000000000..dfaab66e22501c --- /dev/null +++ b/src/common/transformations/include/transformations/op_conversions/convert_scatter_nd_update15_downgrade.hpp @@ -0,0 +1,24 @@ +// Copyright (C) 2018-2023 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "openvino/pass/graph_rewrite.hpp" +#include "transformations_visibility.hpp" + +namespace ov { +namespace pass { +/** + * @ingroup ov_transformation_common_api + * @brief Converts ScatterNDUpdate version 15 to ScatterNDUpdate version 3 if ScatterNDUpdate reduction attribute is set + * to None. + */ +class TRANSFORMATIONS_API ConvertScatterNDUpdate15ToScatterNDUpdate3 : public MatcherPass { +public: + OPENVINO_RTTI("ConvertScatterNDUpdate15ToScatterNDUpdate3", "0"); + ConvertScatterNDUpdate15ToScatterNDUpdate3(); +}; + +} // namespace pass +} // namespace ov diff --git a/src/common/transformations/src/transformations/common_optimizations/common_optimizations.cpp b/src/common/transformations/src/transformations/common_optimizations/common_optimizations.cpp index d7ca44e7ddad34..500d003bd4642e 100644 --- a/src/common/transformations/src/transformations/common_optimizations/common_optimizations.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/common_optimizations.cpp @@ -92,6 +92,7 @@ #include "transformations/op_conversions/convert_roi_align_v3_to_v9.hpp" #include "transformations/op_conversions/convert_roi_align_v9_to_v3.hpp" #include "transformations/op_conversions/convert_scatter_elements_update12_downgrade.hpp" +#include "transformations/op_conversions/convert_scatter_nd_update15_downgrade.hpp" #include "transformations/op_conversions/convert_slice_to_strided_slice.hpp" #include "transformations/op_conversions/convert_softmax_downgrade.hpp" #include "transformations/op_conversions/convert_softmax_upgrade.hpp" @@ -231,6 +232,7 @@ bool ov::pass::CommonOptimizations::run_on_model(const std::shared_ptr(); ADD_MATCHER(fq_fusions, FakeQuantizeMulFusion) diff --git a/src/common/transformations/src/transformations/common_optimizations/nop_elimination.cpp b/src/common/transformations/src/transformations/common_optimizations/nop_elimination.cpp index 13da1c4d9082fc..782c25c2bf03ae 100644 --- a/src/common/transformations/src/transformations/common_optimizations/nop_elimination.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/nop_elimination.cpp @@ -789,8 +789,10 @@ pass::EliminateEltwise::EliminateEltwise() { pass::EliminateScatterUpdate::EliminateScatterUpdate() { MATCHER_SCOPE(EliminateScatterUpdate); - auto scatter_pattern = - pattern::wrap_type(); + auto scatter_pattern = pattern::wrap_type(); matcher_pass_callback callback = [=](pattern::Matcher& m) { auto scatter = m.get_match_root(); diff --git a/src/common/transformations/src/transformations/op_conversions/convert_scatter_nd_update15_downgrade.cpp b/src/common/transformations/src/transformations/op_conversions/convert_scatter_nd_update15_downgrade.cpp new file mode 100644 index 00000000000000..94b2955ee818cc --- /dev/null +++ b/src/common/transformations/src/transformations/op_conversions/convert_scatter_nd_update15_downgrade.cpp @@ -0,0 +1,39 @@ +// Copyright (C) 2018-2023 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "transformations/op_conversions/convert_scatter_nd_update15_downgrade.hpp" + +#include "itt.hpp" +#include "openvino/core/rt_info.hpp" +#include "openvino/op/scatter_nd_update.hpp" +#include "openvino/pass/pattern/op/wrap_type.hpp" +#include "transformations/utils/utils.hpp" + +ov::pass::ConvertScatterNDUpdate15ToScatterNDUpdate3::ConvertScatterNDUpdate15ToScatterNDUpdate3() { + MATCHER_SCOPE(ConvertScatterNDUpdate15ToScatterNDUpdate3); + + const auto scatter_v15_pattern = pattern::wrap_type(); + + const matcher_pass_callback callback = [OV_CAPTURE_CPY_AND_THIS](pattern::Matcher& m) { + const auto scatter_v15 = std::dynamic_pointer_cast(m.get_match_root()); + if (!scatter_v15 || transformation_callback(scatter_v15)) { + return false; + } + if (scatter_v15->get_reduction() != ov::op::v15::ScatterNDUpdate::Reduction::NONE) { + return false; + } + const auto scatter_v3 = std::make_shared(scatter_v15->input_value(0), + scatter_v15->input_value(1), + scatter_v15->input_value(2)); + + scatter_v3->set_friendly_name(scatter_v15->get_friendly_name()); + copy_runtime_info(scatter_v15, scatter_v3); + replace_node(scatter_v15, scatter_v3); + + return true; + }; + + auto m = std::make_shared(scatter_v15_pattern, matcher_name); + register_matcher(m, callback); +} diff --git a/src/common/transformations/tests/common_optimizations/nop_elimination.cpp b/src/common/transformations/tests/common_optimizations/nop_elimination.cpp index e9b4bf3d8be3d1..9ee362104b24d0 100644 --- a/src/common/transformations/tests/common_optimizations/nop_elimination.cpp +++ b/src/common/transformations/tests/common_optimizations/nop_elimination.cpp @@ -1746,3 +1746,25 @@ TEST_F(TransformationTestsF, TransposeElimination) { model_ref = std::make_shared(OutputVector{result}, ParameterVector{data}); } } + +TEST_F(TransformationTestsF, ScatterNDUpdates15Elimination) { + { + auto data = std::make_shared(element::f32, PartialShape{100, 256, 10, 15}); + auto indices = std::make_shared(element::i32, PartialShape{25, 0, 3}); + auto updates = std::make_shared(element::f32, PartialShape{25, 0, 15}); + auto relu = std::make_shared(data); + auto scatter = std::make_shared(relu, indices, updates); + + auto result = std::make_shared(scatter); + model = std::make_shared(OutputVector{result}, ParameterVector{data, indices, updates}); + manager.register_pass(); + } + { + auto data = std::make_shared(element::f32, PartialShape{100, 256, 10, 15}); + auto indices = std::make_shared(element::i32, PartialShape{25, 0, 3}); + auto updates = std::make_shared(element::f32, PartialShape{25, 0, 15}); + auto relu = std::make_shared(data); + auto result = std::make_shared(relu); + model_ref = std::make_shared(OutputVector{result}, ParameterVector{data, indices, updates}); + } +} diff --git a/src/common/transformations/tests/op_conversions/convert_scatter_nd_update15_downgrade_test.cpp b/src/common/transformations/tests/op_conversions/convert_scatter_nd_update15_downgrade_test.cpp new file mode 100644 index 00000000000000..b2a126efe7803e --- /dev/null +++ b/src/common/transformations/tests/op_conversions/convert_scatter_nd_update15_downgrade_test.cpp @@ -0,0 +1,54 @@ +// Copyright (C) 2018-2024 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "transformations/op_conversions/convert_scatter_nd_update15_downgrade.hpp" + +#include + +#include + +#include "common_test_utils/ov_test_utils.hpp" +#include "openvino/opsets/opset15.hpp" +#include "openvino/opsets/opset4.hpp" +#include "openvino/pass/manager.hpp" +#include "transformations/utils/utils.hpp" +using namespace ov; +using namespace testing; + +namespace { +using Reduction = ov::opset15::ScatterNDUpdate::Reduction; + +std::shared_ptr create_v15_model(const Reduction reduction_type) { + const auto data = std::make_shared(ov::element::f32, ov::Shape{1000, 256, 10, 15}); + const auto indices = std::make_shared(ov::element::i32, ov::Shape{25, 125, 3}); + const auto updates = std::make_shared(ov::element::f32, ov::Shape{25, 125, 15}); + const auto scatter_nd = std::make_shared(data, indices, updates, reduction_type); + scatter_nd->set_friendly_name("scatter_nd15"); + return std::make_shared(scatter_nd->outputs(), ov::ParameterVector{data, indices, updates}); +} + +std::shared_ptr create_v3_model() { + const auto data = std::make_shared(ov::element::f32, ov::Shape{1000, 256, 10, 15}); + const auto indices = std::make_shared(ov::element::i32, ov::Shape{25, 125, 3}); + const auto updates = std::make_shared(ov::element::f32, ov::Shape{25, 125, 15}); + const auto scatter_nd = std::make_shared(data, indices, updates); + scatter_nd->set_friendly_name("scatter_nd15"); + + return std::make_shared(scatter_nd->outputs(), ov::ParameterVector{data, indices, updates}); +} + +} // namespace + +TEST_F(TransformationTestsF, ConvertScatterNDUpdate15ToScatterNDUpdate3_no_reduction) { + manager.register_pass(); + model = create_v15_model(Reduction::NONE); + model_ref = create_v3_model(); + comparator.enable(FunctionsComparator::CmpValues::CONST_VALUES); + comparator.enable(FunctionsComparator::CmpValues::ATTRIBUTES); +} + +TEST_F(TransformationTestsF, ConvertScatterNDUpdate15ToScatterNDUpdate3_reduction) { + manager.register_pass(); + model = create_v15_model(Reduction::PROD); +} diff --git a/src/plugins/intel_cpu/src/transformations/transformation_pipeline.cpp b/src/plugins/intel_cpu/src/transformations/transformation_pipeline.cpp index 43862022462ada..54a038c9492db6 100644 --- a/src/plugins/intel_cpu/src/transformations/transformation_pipeline.cpp +++ b/src/plugins/intel_cpu/src/transformations/transformation_pipeline.cpp @@ -58,6 +58,7 @@ #include "transformations/op_conversions/convert_reduce_to_pooling.hpp" #include "transformations/op_conversions/convert_roi_align_v3_to_v9.hpp" #include "transformations/op_conversions/convert_roi_align_v9_to_v3.hpp" +#include "transformations/op_conversions/convert_scatter_nd_update15_downgrade.hpp" #include "transformations/op_conversions/convert_sequences_to_tensor_iterator.hpp" #include "transformations/op_conversions/convert_shuffle_channels3.hpp" #include "transformations/op_conversions/convert_slice_to_strided_slice.hpp" @@ -636,6 +637,7 @@ void Transformations::PreLpt(const std::vector& defaultPrecis CPU_DISABLE_PASS_COMMON(manager, ov::pass::ConvertTopK11ToTopK3); CPU_DISABLE_PASS_COMMON(manager, ov::pass::HSwishDecomposition); CPU_DISABLE_PASS_COMMON(manager, ov::pass::MatMulConstTransposesExtraction); + CPU_DISABLE_PASS_COMMON(manager, ov::pass::ConvertScatterNDUpdate15ToScatterNDUpdate3); CPU_DISABLE_PASS_X64(manager, ov::pass::HSigmoidDecomposition); CPU_DISABLE_PASS_X64(manager, ov::pass::ReduceL1Decomposition); diff --git a/tests/layer_tests/tensorflow_tests/test_tf_SparseTensorDenseMatMul.py b/tests/layer_tests/tensorflow_tests/test_tf_SparseTensorDenseMatMul.py index 1eb31a753b6bfd..49df4226a7fcb2 100644 --- a/tests/layer_tests/tensorflow_tests/test_tf_SparseTensorDenseMatMul.py +++ b/tests/layer_tests/tensorflow_tests/test_tf_SparseTensorDenseMatMul.py @@ -91,8 +91,6 @@ def test_sparse_tensor_dense_mat_mul(self, data_type, indices_type, a_shape, b_shape, nnz, ie_device, precision, ir_version, temp_dir, use_legacy_frontend): - if ie_device == 'GPU': - pytest.skip("149830: ScatterNDUpdate-15 is not supported on GPU") self._test(*self.create_sparse_tensor_dense_mat_mul_net(data_type, indices_type, adjoint_a, adjoint_b, a_shape, b_shape, nnz), From 0ced13225dcdb83833c89e24b3fb1fcdd395a948 Mon Sep 17 00:00:00 2001 From: Xiake Sun Date: Fri, 23 Aug 2024 14:25:34 +0800 Subject: [PATCH 31/35] Fix performance profiling on GPU with oneDNN support (#26174) Fix performance profiling on GPU with oneDNN support, so that benchmark_app can get performance counter and dump exec graph on DG2 and further platform with oneDNN support. CVS-150346 --------- Co-authored-by: Sergey Shlyapnikov --- .../src/graph/impls/onednn/primitive_onednn_base.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/plugins/intel_gpu/src/graph/impls/onednn/primitive_onednn_base.h b/src/plugins/intel_gpu/src/graph/impls/onednn/primitive_onednn_base.h index 573c29b115842e..57fd4afbe933d6 100644 --- a/src/plugins/intel_gpu/src/graph/impls/onednn/primitive_onednn_base.h +++ b/src/plugins/intel_gpu/src/graph/impls/onednn/primitive_onednn_base.h @@ -560,10 +560,14 @@ struct typed_primitive_onednn_impl : public typed_primitive_impl { stream.wait(); std::vector duration = dnnl::get_profiling_data(stream.get_onednn_stream(), dnnl::profiling_data_kind::time); - OPENVINO_ASSERT(duration.size() == 1, "[GPU] oneDNN profiling data is expected to have info only for single primitive ", + if (duration.empty()) { + event = std::make_shared(0); + } else { + OPENVINO_ASSERT(duration.size() == 1, "[GPU] oneDNN profiling data is expected to have info only for single primitive ", "actual number is ", duration.size()); + event = std::make_shared(duration[0]); + } - event = std::make_shared(duration[0]); } else { // If oneDNN primitive is the output primitive or it's user is CPU implementation, then enqueue marker // with empty events wait list (which will trigger wait for all previously enqueued tasks) and From 2fd35c95acd379a41601208258e4056bed7377e3 Mon Sep 17 00:00:00 2001 From: David Pava Date: Fri, 23 Aug 2024 09:29:48 +0300 Subject: [PATCH 32/35] [NPU] Removing a ref to fix workload type testing errors (#26186) ### Details: - Removing a ref to fix workload type testing errors ### Tickets: --- src/plugins/intel_npu/src/plugin/src/compiled_model.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/intel_npu/src/plugin/src/compiled_model.cpp b/src/plugins/intel_npu/src/plugin/src/compiled_model.cpp index 54942d1416208e..d584c62704fbf9 100644 --- a/src/plugins/intel_npu/src/plugin/src/compiled_model.cpp +++ b/src/plugins/intel_npu/src/plugin/src/compiled_model.cpp @@ -214,7 +214,7 @@ void CompiledModel::configure_stream_executors() { } void CompiledModel::initialize_properties() { - const auto& pluginSupportedProperties = + const auto pluginSupportedProperties = get_plugin()->get_property(ov::supported_properties.name(), {}).as>(); const auto isPropertySupported = [&pluginSupportedProperties](const std::string& name) { return std::any_of(pluginSupportedProperties.begin(), From 36e7b30b4fc8a3ec9c9112fba72b9c3c9b9c8d24 Mon Sep 17 00:00:00 2001 From: Oleg Pipikin Date: Fri, 23 Aug 2024 08:50:35 +0200 Subject: [PATCH 33/35] Optimization of raw meta data serialization (#26115) ### Details: - Optimization of raw meta data serialization - Fix legacy meta data creation ### Tickets: - CVS-123381 --- src/core/dev_api/openvino/core/meta_data.hpp | 14 ++++ src/core/src/pass/serialize.cpp | 22 ++++-- .../serialization/rt_info_serialization.cpp | 78 +++++++++++++++++++ src/frontends/ir/src/ir_deserializer.cpp | 67 ++++++++++------ 4 files changed, 153 insertions(+), 28 deletions(-) diff --git a/src/core/dev_api/openvino/core/meta_data.hpp b/src/core/dev_api/openvino/core/meta_data.hpp index 3b0debaff3763e..221aca96080d65 100644 --- a/src/core/dev_api/openvino/core/meta_data.hpp +++ b/src/core/dev_api/openvino/core/meta_data.hpp @@ -6,6 +6,10 @@ #include "openvino/core/any.hpp" +namespace pugi { +class xml_node; +} + namespace ov { /** @@ -32,4 +36,14 @@ class OPENVINO_API Meta { virtual ~Meta() = default; }; +class MetaDataWithPugixml : public Meta { +public: + /** + * @brief Returns meta unchanged meta information. Throws ov::Exception if the meta was potentially changed + * + * @return const pugi::xml_node& with meta information + */ + virtual const pugi::xml_node& get_pugi_node() const = 0; +}; + } // namespace ov diff --git a/src/core/src/pass/serialize.cpp b/src/core/src/pass/serialize.cpp index 17da51b5831c99..1d5e295e8dbe23 100644 --- a/src/core/src/pass/serialize.cpp +++ b/src/core/src/pass/serialize.cpp @@ -916,11 +916,23 @@ void serialize_rt_info(pugi::xml_node& root, const std::string& name, const ov:: child.append_attribute("name").set_value(name.c_str()); } if (data.is>()) { - std::shared_ptr meta = data.as>(); - ov::AnyMap& map = *meta; - for (const auto& it : map) { - serialize_rt_info(child, it.first, it.second); - } + auto meta = data.as>(); + do { + if (auto meta_with_pugixml_node = std::dynamic_pointer_cast(meta)) { + if (auto pugi_node = meta_with_pugixml_node->get_pugi_node()) { + root.remove_child(child); + auto added_node = root.append_copy(pugi_node); + OPENVINO_ASSERT(added_node, "Cannot add pugixml node with name: ", name); + added_node.set_name(name.c_str()); + break; + } + } + // Meta in ov::Meta cannot be accessed by MetaDataWithPugixml::get_pugi_node. Read it as ov::AnyMap + ov::AnyMap& map = *meta; + for (const auto& it : map) { + serialize_rt_info(child, it.first, it.second); + } + } while (false); } else if (data.is()) { const ov::AnyMap& any_map = data.as(); for (const auto& it : any_map) { diff --git a/src/core/tests/pass/serialization/rt_info_serialization.cpp b/src/core/tests/pass/serialization/rt_info_serialization.cpp index 2664ab5edca18a..412625d45d53d9 100644 --- a/src/core/tests/pass/serialization/rt_info_serialization.cpp +++ b/src/core/tests/pass/serialization/rt_info_serialization.cpp @@ -10,6 +10,7 @@ #include "openvino/frontend/manager.hpp" #include "openvino/opsets/opset8.hpp" #include "openvino/pass/manager.hpp" +#include "openvino/runtime/core.hpp" #include "transformations/rt_info/attributes.hpp" class RTInfoSerializationTest : public ov::test::TestsCommon { @@ -221,3 +222,80 @@ TEST_F(RTInfoSerializationTest, tag_names_verification) { ASSERT_EQ(model_rt_info[item.first], item.second); }); } + +TEST(OvSerializationTests, SerializeRawMeta) { + std::string ir_with_rt_info = R"V0G0N( + + + + + + + 3 + 20 + 20 + + + + + + + 3 + 20 + 20 + + + + + + + + + + + + + + + + +)V0G0N"; + ov::Core core; + { + // Don't read meta data. Copy raw pugixml::node from MetaDataWithPugixml to serialized model + auto model = core.read_model(ir_with_rt_info, ov::Tensor()); + + std::stringstream model_ss, weights_ss; + ov::pass::Serialize(model_ss, weights_ss).run_on_model(model); + + auto serialized_model = model_ss.str(); + EXPECT_EQ(0, serialized_model.compare(ir_with_rt_info)); + } + + { + // Don't read meta data. Fully serialize AnyMap with meta + auto model = core.read_model(ir_with_rt_info, ov::Tensor()); + auto custom_rt_info1_value = model->get_rt_info("custom_rt_info1", "item0"); + EXPECT_EQ(0, custom_rt_info1_value.compare("testvalue1")); + auto custom_rt_info2_value = model->get_rt_info("custom_rt_info2", "item0"); + EXPECT_EQ(0, custom_rt_info2_value.compare("testvalue2")); + + std::stringstream model_ss, weights_ss; + ov::pass::Serialize(model_ss, weights_ss).run_on_model(model); + + auto serialized_model = model_ss.str(); + EXPECT_EQ(0, serialized_model.compare(ir_with_rt_info)); + } + + { + auto model = core.read_model(ir_with_rt_info, ov::Tensor()); + auto custom_rt_info1_value = model->get_rt_info("custom_rt_info1", "item0"); + EXPECT_EQ(0, custom_rt_info1_value.compare("testvalue1")); + + std::stringstream model_ss, weights_ss; + ov::pass::Serialize(model_ss, weights_ss).run_on_model(model); + + auto serialized_model = model_ss.str(); + EXPECT_EQ(0, serialized_model.compare(ir_with_rt_info)); + } +} diff --git a/src/frontends/ir/src/ir_deserializer.cpp b/src/frontends/ir/src/ir_deserializer.cpp index 8c8aec333e107e..68900b150514bc 100644 --- a/src/frontends/ir/src/ir_deserializer.cpp +++ b/src/frontends/ir/src/ir_deserializer.cpp @@ -584,10 +584,17 @@ std::shared_ptr ov::XmlDeserializer::parse_function(const pugi::xml_n return function; } -class MetaDataParser : public ov::Meta { +class MetaDataParser : public ov::MetaDataWithPugixml { public: - MetaDataParser(const std::string& name, const pugi::xml_node& meta) : m_name(name) { + MetaDataParser(const std::string& name, const pugi::xml_node& meta, bool accessible_by_pugixml_node = true) + : m_name(name), + m_accessible_by_pugixml_node(accessible_by_pugixml_node) { m_meta.append_copy(meta); + if (accessible_by_pugixml_node) { + m_meta_node = m_meta.child(m_name.c_str()); + } else { + m_meta_node = pugi::xml_node(); + } } operator const ov::AnyMap&() const override { @@ -600,6 +607,14 @@ class MetaDataParser : public ov::Meta { return m_parsed_map; } + const pugi::xml_node& get_pugi_node() const override { + if (!m_meta_node.empty() && !m_accessible_by_pugixml_node) { + // Meta cannot be accessed by pugixml node. Return empty node + m_meta_node = pugi::xml_node(); + } + return m_meta_node; + }; + private: bool has_attr(const pugi::xml_node& node, const std::string& name = "value") const { auto attr = node.attribute(name.c_str()); @@ -646,14 +661,18 @@ class MetaDataParser : public ov::Meta { void parse() const { std::call_once(m_oc, [this]() { + m_accessible_by_pugixml_node = false; const pugi::xml_node& node = m_meta.child(m_name.c_str()); m_parsed_map = parse_node(node); }); } + pugi::xml_document m_meta; const std::string m_name; mutable ov::AnyMap m_parsed_map; mutable std::once_flag m_oc; + mutable std::atomic_bool m_accessible_by_pugixml_node; + mutable pugi::xml_node m_meta_node; }; void ov::XmlDeserializer::read_meta_data(const std::shared_ptr& model, const pugi::xml_node& meta_section) { @@ -681,29 +700,31 @@ void ov::XmlDeserializer::read_meta_data(const std::shared_ptr& model void ov::XmlDeserializer::read_legacy_meta_data(const std::shared_ptr& model, const std::unordered_set& names, const pugi::xml_node& root_section) { - const auto& read_meta = [](const std::shared_ptr& model, - const std::string& name, - const pugi::xml_node& meta_section) { - auto& rt_info = model->get_rt_info(); - if (name == "meta_data") { - for (const auto& data : meta_section.children()) { - const std::string& section_name = data.name(); - // Rename cli_parameters to conversion_parameters - if (section_name == "cli_parameters") { - std::shared_ptr meta = std::make_shared("cli_parameters", data); - rt_info["conversion_parameters"] = meta; - } else if (!data.attribute("value").empty()) { - rt_info[data.name()] = pugixml::get_str_attr(data, "value"); - } else { - OPENVINO_THROW("Unsupported legacy argument: ", data.name()); + const auto& read_meta = + [](const std::shared_ptr& model, const std::string& name, const pugi::xml_node& meta_section) { + auto& rt_info = model->get_rt_info(); + if (name == "meta_data") { + for (const auto& data : meta_section.children()) { + const std::string& section_name = data.name(); + // Rename cli_parameters to conversion_parameters + if (section_name == "cli_parameters") { + std::shared_ptr meta = std::make_shared("cli_parameters", data); + rt_info["conversion_parameters"] = meta; + } else if (!data.attribute("value").empty()) { + rt_info[data.name()] = pugixml::get_str_attr(data, "value"); + } else { + OPENVINO_THROW("Unsupported legacy argument: ", data.name()); + } } + } else if (name == "quantization_parameters") { + // Rename quantization_parameters to optimization + // Legacy implementation. Have to be parsed inside MetaDataParser. Do not allow to serialize it as raw + // pugi::xml_node. + std::shared_ptr meta = + std::make_shared("quantization_parameters", meta_section, false); + rt_info["optimization"] = meta; } - } else if (name == "quantization_parameters") { - // Rename quantization_parameters to optimization - std::shared_ptr meta = std::make_shared("quantization_parameters", meta_section); - rt_info["optimization"] = meta; - } - }; + }; for (const auto& it : names) read_meta(model, it, root_section.child(it.c_str())); } From 8a08bb9c5f901da89596070d189d2aa67879de34 Mon Sep 17 00:00:00 2001 From: tadamczx <156996781+tadamczx@users.noreply.github.com> Date: Fri, 23 Aug 2024 08:51:47 +0200 Subject: [PATCH 34/35] [DOCS] Simplified snippets setting to work in every environment (#26193) ### Details: - *item1* - *...* ### Tickets: - *ticket-id* --- docs/sphinx_setup/conf.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/sphinx_setup/conf.py b/docs/sphinx_setup/conf.py index 5b679d10213774..ea144ca84d6154 100644 --- a/docs/sphinx_setup/conf.py +++ b/docs/sphinx_setup/conf.py @@ -113,8 +113,6 @@ "show_prev_next": False, } -snippet_root = os.getenv("SNIPPET_ROOT", "") - html_sidebars = { "**": ["search-field.html", "sidebar-nav-bs.html", "sidebar-ethical-ads.html"] } @@ -123,7 +121,8 @@ 'current_language': 'English', #'languages': (('English', '/latest'), ('Chinese', '/cn/latest')), 'doxygen_mapping_file': '@DOXYGEN_MAPPING_FILE@', - 'doxygen_snippet_root': snippet_root, + # go back fours 3 steps down in directory to reach openvino dir + 'doxygen_snippet_root': os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../')), 'default_mode': 'light' } From 6d0d7716ac193e77ee1e63013c3072e3984f746e Mon Sep 17 00:00:00 2001 From: Roman Kazantsev Date: Fri, 23 Aug 2024 16:20:36 +0400 Subject: [PATCH 35/35] [MO Tests] Switch off legacy test_reduce_lp test (#26196) **Details:** Switch off legacy test_reduce_lp test **Ticket:** 150423 --------- Signed-off-by: Kazantsev, Roman --- tools/mo/unit_tests/mo/ops/ReduceOps_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/mo/unit_tests/mo/ops/ReduceOps_test.py b/tools/mo/unit_tests/mo/ops/ReduceOps_test.py index f1cbc19d331a3a..947fc95c38ab9f 100644 --- a/tools/mo/unit_tests/mo/ops/ReduceOps_test.py +++ b/tools/mo/unit_tests/mo/ops/ReduceOps_test.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 import pytest +import unittest import numpy as np @@ -21,6 +22,7 @@ class TestReduceLpTest(): + @unittest.skip("Skipped due to function array_equal failure") @pytest.mark.parametrize("shape, axes, keepdims, p",[ ([3, 2, 2], [0], True, 1), ([3, 2, 2], [0], True, 2),