From 4e9f12928e0d7314b9043727f4b6ebc5f97a5ce5 Mon Sep 17 00:00:00 2001 From: Tim Ward Date: Thu, 1 Aug 2019 10:26:38 +0100 Subject: [PATCH 1/3] Make the shaded Jackson module output an OSGi bundle * Minimal changes to the module build, although it appears that this could be simplified * All Jackson packages exported at the jackson.version Signed-off-by: Tim Ward --- nd4j/nd4j-shade/jackson/bnd.bnd | 16 ++++++++++++++++ nd4j/nd4j-shade/jackson/pom.xml | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 nd4j/nd4j-shade/jackson/bnd.bnd diff --git a/nd4j/nd4j-shade/jackson/bnd.bnd b/nd4j/nd4j-shade/jackson/bnd.bnd new file mode 100644 index 000000000000..fb316a85116c --- /dev/null +++ b/nd4j/nd4j-shade/jackson/bnd.bnd @@ -0,0 +1,16 @@ +# This file configures the package exports and versions for the +# shaded jackson implementation. Normally annotations would be +# used, but there is no source code for a shaded jar. + +# We export everything which is a bad practice, but it's +# unclear which packages are actually needed by nd4j. +# In future this can be improved by making a more restrictive +# list of exported backages +Export-Package: org.nd4j.shade.jackson.*;version=${jackson.version} + +# Exclude the import for the jackson jaxb module which isn't +# provided by ND4J and isn't used. This helps to keep the +# module behaviour consistent and clean, as well as avoiding +# an unversioned import (an error-prone bad practice) +Import-Package: !org.nd4j.shade.jackson.module.jaxb,\ + * \ No newline at end of file diff --git a/nd4j/nd4j-shade/jackson/pom.xml b/nd4j/nd4j-shade/jackson/pom.xml index 1d53e1c41354..b08d66f90871 100644 --- a/nd4j/nd4j-shade/jackson/pom.xml +++ b/nd4j/nd4j-shade/jackson/pom.xml @@ -30,6 +30,7 @@ true + org.nd4j.shade.jackson @@ -157,6 +158,11 @@ maven-jar-plugin true + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + @@ -212,6 +218,18 @@ + + biz.aQute.bnd + bnd-maven-plugin + 4.2.0 + + + + bnd-process + + + + From bfc2503f7b83b2f0bc4af1d1b2cafeeb5c4076e2 Mon Sep 17 00:00:00 2001 From: Tim Ward Date: Thu, 1 Aug 2019 10:04:35 +0100 Subject: [PATCH 2/3] Create an OSGi bundle for the main ND4J API This is the least invasive option for creating an OSGi enabled ND4J bundle, but it could be improved * Multiple API jars are combined to avoid split package problems, ideally there would be no split packages * API packages and their versions are determined here, ideally this would be done using annotations in the packages themselves * Some third party code is included because it is not packaged in OSGi bundles * Some third party code leaks out through the ND4J API Signed-off-by: Tim Ward --- nd4j/nd4j-osgi/bnd.bnd | 62 ++++++++++++++++++++++++ nd4j/nd4j-osgi/pom.xml | 104 +++++++++++++++++++++++++++++++++++++++++ nd4j/pom.xml | 1 + 3 files changed, 167 insertions(+) create mode 100644 nd4j/nd4j-osgi/bnd.bnd create mode 100644 nd4j/nd4j-osgi/pom.xml diff --git a/nd4j/nd4j-osgi/bnd.bnd b/nd4j/nd4j-osgi/bnd.bnd new file mode 100644 index 000000000000..ad94bf28fea7 --- /dev/null +++ b/nd4j/nd4j-osgi/bnd.bnd @@ -0,0 +1,62 @@ +# This file is used to configure the bnd-maven-plugin and to generate OSGi +# metadata for the "frontend" bundle + +Bundle-SymbolicName: org.nd4j.frontend + +# Export all the packages from ND4J, except the ones with "impl" in the package name +api.package.names: ${filterout;${packages;NAMED;org.nd4j.*|onnx|org.tensorflow.*|tensorflow*};impl} + +# For now, all the API packages use the same version, taken from the maven version. +# Ideally the package versions would be maintained individually using annotations +api.version: ${maven_version;${project.version}} + +# Assemble the full export string containing all the packages, versions, and directives +# Note that the suffix prevents the API packages from being substitutably imported +api.export.string: ${replace;${api.package.names};$;\\;version=${api.version}\\;-noimport:=true} + + +# We export all of the ND4J API, plus any third party API that we contain which is exposed +# through the ND4J API. Note that exposing third party API through your API constitutes +# tight coupling and is evidence of a leaky abstraction. It may be that too many packages +# from ND4J are being considered API and that they should be kept private. This would allow +# the third party package exports to be removed. +# +# JavaCPP is exposed in lots of places, NeoIterTools is exposed via +# org.nd4j.linalg.indexing. The shaded Protobuf is also exposed through several parts of +# the ND4J API. +# Take care with these dependencies as there are no variables for some of the versions. + +-exportcontents: \ + ${api.export.string},\ + com.github.os72.protobuf351;version=0.9,\ + org.bytedeco.javacpp;version=${javacpp.version},\ + org.bytedeco.javacpp.annotation;version=${javacpp.version},\ + org.bytedeco.javacpp.indexer;version=${javacpp.version},\ + org.bytedeco.javacpp.tools;version=${javacpp.version},\ + net.ericaro.neoitertools;version=1.0.0 + +# Embed the frontend jar files produced by ND4J. These must be delivered together +# due to split packages between the various jar files +-includeresource: \ + @nd4j-common-${project.version}.jar,\ + @nd4j-context-${project.version}.jar,\ + @nd4j-buffer-${project.version}.jar,\ + @nd4j-api-${project.version}.jar,\ + @nd4j-native-api-${project.version}.jar + + # These are non-OSGi packaged dependencies which we therefore merge + # into this OSGi bundle as private copies + -conditionalpackage: \ + com.github.os72.protobuf351.*,\ + com.jakewharton.byteunits.*,\ + org.bytedeco.javacpp.*,\ + net.ericaro.neoitertools.*,\ + oshi.* + + # These dependencies must be customised to remove them or make them + # optional. The code paths that use them are not exercised by ND4J + Import-Package: \ + !android.content,\ + !org.apache.maven.*,\ + sun.misc;resolution:=optional,\ + * \ No newline at end of file diff --git a/nd4j/nd4j-osgi/pom.xml b/nd4j/nd4j-osgi/pom.xml new file mode 100644 index 000000000000..9a2090e690b4 --- /dev/null +++ b/nd4j/nd4j-osgi/pom.xml @@ -0,0 +1,104 @@ + + + + + nd4j + org.nd4j + 1.0.0-SNAPSHOT + + 4.0.0 + + nd4j-osgi + jar + + nd4j-osgi + + This module takes the ND4J frontend and packages it as an OSGi bundle. + It is necessary to repackage in this way due to the prevalence of split + packages that exist between various ND4J modules, which prevent the + individual jar files being used as standalone OSGi bundles + + + + + + + biz.aQute.bnd + bnd-maven-plugin + ${bnd.version} + + + + bnd-process + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.0.2 + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + true + + + + + + + + + + org.nd4j + nd4j-common + ${project.version} + + + org.nd4j + nd4j-context + ${project.version} + + + org.nd4j + nd4j-buffer + ${project.version} + + + org.nd4j + nd4j-api + ${project.version} + + + org.nd4j + nd4j-native-api + ${project.version} + + + + + 1.7 + 1.7 + 4.2.0 + + + + diff --git a/nd4j/pom.xml b/nd4j/pom.xml index 255859171c26..bdb0f4c9113b 100644 --- a/nd4j/pom.xml +++ b/nd4j/pom.xml @@ -61,6 +61,7 @@ nd4j-backends nd4j-parameter-server-parent nd4j-uberjar + nd4j-osgi nd4j-tensorflow From 04fa6a9e9c486801147f89e239414afa23663f3f Mon Sep 17 00:00:00 2001 From: Tim Ward Date: Mon, 5 Aug 2019 13:10:36 +0100 Subject: [PATCH 3/3] Create an OSGi bundle for the ND4J Native CPU backend This is the least invasive option for creating an ND4J backend * The Native Java API, Java Implementation and Native code are all combined into one module * Each platform creates its own specific OSGi bundle complete with native dependencies * The Frontend has a requirement on at least one backend being present * The Frontend logic has been improved to handle backend discovery/loading when it's a separate module * A simple OSGi sniff test has been added to demonstrate loading an ND Array Signed-off-by: Tim Ward --- .../nd4j-api-parent/nd4j-api/pom.xml | 5 + .../compression/BasicNDArrayCompressor.java | 59 +++++++- .../java/org/nd4j/linalg/factory/Nd4j.java | 67 +++++---- .../nd4j-native-osgi/bnd.bnd | 53 +++++++ .../nd4j-native-osgi/pom.xml | 95 ++++++++++++ .../nd4j-native-platform/pom.xml | 67 +++++++++ nd4j/nd4j-backends/nd4j-backend-impls/pom.xml | 1 + nd4j/nd4j-backends/nd4j-tests-osgi/bnd.bnd | 1 + nd4j/nd4j-backends/nd4j-tests-osgi/cpu.bndrun | 29 ++++ nd4j/nd4j-backends/nd4j-tests-osgi/pom.xml | 140 ++++++++++++++++++ .../nd4j/test/osgi/NDArrayCreationTest.java | 88 +++++++++++ nd4j/nd4j-backends/pom.xml | 1 + nd4j/nd4j-context/pom.xml | 5 + .../org/nd4j/linalg/factory/Nd4jBackend.java | 39 ++++- nd4j/nd4j-osgi/bnd.bnd | 20 ++- nd4j/pom.xml | 6 + pom.xml | 3 + 17 files changed, 644 insertions(+), 35 deletions(-) create mode 100644 nd4j/nd4j-backends/nd4j-backend-impls/nd4j-native-osgi/bnd.bnd create mode 100644 nd4j/nd4j-backends/nd4j-backend-impls/nd4j-native-osgi/pom.xml create mode 100644 nd4j/nd4j-backends/nd4j-tests-osgi/bnd.bnd create mode 100644 nd4j/nd4j-backends/nd4j-tests-osgi/cpu.bndrun create mode 100644 nd4j/nd4j-backends/nd4j-tests-osgi/pom.xml create mode 100644 nd4j/nd4j-backends/nd4j-tests-osgi/src/main/java/org/nd4j/test/osgi/NDArrayCreationTest.java diff --git a/nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/pom.xml b/nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/pom.xml index 7168b57e0852..dcb856407d4a 100644 --- a/nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/pom.xml +++ b/nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/pom.xml @@ -140,6 +140,11 @@ commons-math3 ${commons-math3.version} + + + org.osgi + osgi.core + diff --git a/nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/src/main/java/org/nd4j/linalg/compression/BasicNDArrayCompressor.java b/nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/src/main/java/org/nd4j/linalg/compression/BasicNDArrayCompressor.java index 665d6ce19cf8..c6da198a032f 100644 --- a/nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/src/main/java/org/nd4j/linalg/compression/BasicNDArrayCompressor.java +++ b/nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/src/main/java/org/nd4j/linalg/compression/BasicNDArrayCompressor.java @@ -23,7 +23,20 @@ import org.nd4j.linalg.api.buffer.DataType; import org.nd4j.linalg.api.ndarray.INDArray; import org.nd4j.linalg.factory.Nd4j; +import org.nd4j.linalg.factory.Nd4jBackend; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.namespace.PackageNamespace; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; +import com.google.common.collect.Iterables; +import com.google.common.collect.Iterators; + +import static org.osgi.framework.namespace.PackageNamespace.PACKAGE_NAMESPACE; + +import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.ServiceLoader; import java.util.Set; @@ -49,9 +62,12 @@ protected void loadCompressors() { We scan classpath for NDArrayCompressor implementations and add them one by one to codecs map */ codecs = new ConcurrentHashMap<>(); + + + Iterable discoveredCompressors = serviceLoaderDiscovery(NDArrayCompressor.class); + - ServiceLoader loader = ServiceLoader.load(NDArrayCompressor.class); - for (NDArrayCompressor compressor : loader) { + for (NDArrayCompressor compressor : discoveredCompressors) { codecs.put(compressor.getDescriptor().toUpperCase(), compressor); } @@ -65,6 +81,45 @@ protected void loadCompressors() { } } + private static Iterable serviceLoaderDiscovery(Class iface) { + + // Always use the default service loader behaviour (TCCL) + Iterable services = ServiceLoader.load(iface); + + try { + Bundle bundle = FrameworkUtil.getBundle(iface); + + if(bundle != null) { + // This ND4J is running in an OSGi framework, make sure we search + // 1. The API bundle (to pick up any default handlers) + // 2. Any other bundles that import the service API package + + services = Iterables.concat(services, ServiceLoader.load(iface, iface.getClassLoader())); + + BundleWiring wiring = bundle.adapt(BundleWiring.class); + List packageConsumers = wiring.getProvidedWires(PACKAGE_NAMESPACE); + + if(packageConsumers != null) { + for(BundleWire wire : packageConsumers) { + if(!NDArrayCompressor.class.getPackage().getName().equals( + wire.getCapability().getAttributes().get(PACKAGE_NAMESPACE))) { + continue; + } + + ClassLoader providerClassLoader = wire.getRequirerWiring().getClassLoader(); + services = Iterables.concat(services, + ServiceLoader.load(iface, providerClassLoader)); + } + } + + } + + } catch (NoClassDefFoundError e) { + // We're not running in OSGi + } + return services; + } + /** * Get the set of available codecs for * compression diff --git a/nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/src/main/java/org/nd4j/linalg/factory/Nd4j.java b/nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/src/main/java/org/nd4j/linalg/factory/Nd4j.java index b82ce008ac80..d7a7900ea7cd 100644 --- a/nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/src/main/java/org/nd4j/linalg/factory/Nd4j.java +++ b/nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/src/main/java/org/nd4j/linalg/factory/Nd4j.java @@ -5492,46 +5492,50 @@ public void initWithBackend(Nd4jBackend backend) { DataTypeUtil.setDTypeForContext(dtype); } + ClassLoader backendLoader = backend.getClass().getClassLoader(); + compressDebug = pp.toBoolean(COMPRESSION_DEBUG); char ORDER = pp.toChar(ORDER_KEY, NDArrayFactory.C); + + // These types have no default and must be loaded from the plugin classloader Class affinityManagerClazz = (Class) Class - .forName(pp.toString(AFFINITY_MANAGER)); + .forName(pp.toString(AFFINITY_MANAGER), true, backendLoader); affinityManager = affinityManagerClazz.newInstance(); Class ndArrayFactoryClazz = (Class) Class.forName( - pp.toString(NDARRAY_FACTORY_CLASS)); + pp.toString(NDARRAY_FACTORY_CLASS), true, backendLoader); Class sparseNDArrayClazz = (Class) Class.forName( - pp.toString(SPARSE_NDARRAY_FACTORY_CLASS)); - Class convolutionInstanceClazz = (Class) Class - .forName(pp.toString(CONVOLUTION_OPS, DefaultConvolutionInstance.class.getName())); - String defaultName = pp.toString(DATA_BUFFER_OPS, DefaultDataBufferFactory.class.getName()); - Class dataBufferFactoryClazz = (Class) Class - .forName(pp.toString(DATA_BUFFER_OPS, defaultName)); + pp.toString(SPARSE_NDARRAY_FACTORY_CLASS), true, backendLoader); Class shapeInfoProviderClazz = (Class) Class - .forName(pp.toString(SHAPEINFO_PROVIDER)); + .forName(pp.toString(SHAPEINFO_PROVIDER), true, backendLoader); Class sparseInfoProviderClazz = (Class) Class.forName( - pp.toString(SPARSEINFO_PROVIDER)); - + pp.toString(SPARSEINFO_PROVIDER), true, backendLoader); Class constantProviderClazz = (Class) Class - .forName(pp.toString(CONSTANT_PROVIDER)); - + .forName(pp.toString(CONSTANT_PROVIDER), true, backendLoader); Class memoryManagerClazz = (Class) Class - .forName(pp.toString(MEMORY_MANAGER)); + .forName(pp.toString(MEMORY_MANAGER), true, backendLoader); + Class workspaceManagerClazz = (Class) Class + .forName(pp.toString(WORKSPACE_MANAGER), true, backendLoader); + Class blasWrapperClazz = (Class) Class + .forName(pp.toString(BLAS_OPS), true, backendLoader); + Class sparseBlasWrapperClazz = (Class) Class + .forName(pp.toString(SPARSE_BLAS_OPS), true, backendLoader); + + // These types have defaults and so we use the getPluginClass method + Class convolutionInstanceClazz = getPluginClass(pp, CONVOLUTION_OPS, + DefaultConvolutionInstance.class, backendLoader); + Class dataBufferFactoryClazz = getPluginClass(pp, DATA_BUFFER_OPS, + DefaultDataBufferFactory.class, backendLoader); + allowsOrder = backend.allowsOrder(); - String rand = pp.toString(RANDOM_PROVIDER, DefaultRandom.class.getName()); - Class randomClazz = (Class) Class.forName(rand); + Class randomClazz = getPluginClass(pp, RANDOM_PROVIDER, + DefaultRandom.class, backendLoader); randomFactory = new RandomFactory(randomClazz); - Class workspaceManagerClazz = (Class) Class - .forName(pp.toString(WORKSPACE_MANAGER)); - Class blasWrapperClazz = (Class) Class - .forName(pp.toString(BLAS_OPS)); - Class sparseBlasWrapperClazz = (Class) Class - .forName(pp.toString(SPARSE_BLAS_OPS)); - String clazzName = pp.toString(DISTRIBUTION, DefaultDistributionFactory.class.getName()); - Class distributionFactoryClazz = (Class) Class.forName(clazzName); + Class distributionFactoryClazz = getPluginClass(pp, DISTRIBUTION, + DefaultDistributionFactory.class, backendLoader); memoryManager = memoryManagerClazz.newInstance(); @@ -5540,8 +5544,8 @@ public void initWithBackend(Nd4jBackend backend) { sparseInfoProvider = sparseInfoProviderClazz.newInstance(); workspaceManager = workspaceManagerClazz.newInstance(); - Class opExecutionerClazz = (Class) Class - .forName(pp.toString(OP_EXECUTIONER, DefaultOpExecutioner.class.getName())); + Class opExecutionerClazz = getPluginClass(pp, OP_EXECUTIONER, + DefaultOpExecutioner.class, backendLoader); OP_EXECUTIONER_INSTANCE = opExecutionerClazz.newInstance(); Constructor c2 = ndArrayFactoryClazz.getConstructor(DataType.class, char.class); @@ -5590,6 +5594,17 @@ public void initWithBackend(Nd4jBackend backend) { } + private Class getPluginClass(PropertyParser pp, String key, + Class defaultClass, ClassLoader pluginClassLoader) throws ClassNotFoundException { + String className = pp.getProperties().getProperty(key); + + if(className == null) { + return defaultClass; + } else { + return (Class) Class.forName(className, true, pluginClassLoader); + } + } + private static boolean isSupportedPlatform() { return (System.getProperty("java.vm.name").equalsIgnoreCase("Dalvik") || System.getProperty("os.arch").toLowerCase().startsWith("arm") diff --git a/nd4j/nd4j-backends/nd4j-backend-impls/nd4j-native-osgi/bnd.bnd b/nd4j/nd4j-backends/nd4j-backend-impls/nd4j-native-osgi/bnd.bnd new file mode 100644 index 000000000000..7224f0c491c3 --- /dev/null +++ b/nd4j/nd4j-backends/nd4j-backend-impls/nd4j-native-osgi/bnd.bnd @@ -0,0 +1,53 @@ +# This file configures the Manifest for the native backend bundle + +Bundle-SymbolicName: org.nd4j.backend.native + +# A safe version to use in dependency matching +safe.version: ${maven_version;${project.version}} + +Bundle-Version: ${safe.version} + +# Java CPP requires that the JNI classes and the Java CPP library are loaded from +# the same classloader. JavaCPP is packaged in with the frontend, so we attach as +# a fragment to ensure we have a single flat class space. Ideally JavaCPP wouldn't +# require this and then the backend could be a totally separate module. + +Fragment-Host: org.nd4j.frontend;bundle-version="[${safe.version},${safe.version}]" + +# These packages are needed by the generated code +-conditionalpackage: \ + org.bytedeco.mkl.*,\ + org.bytedeco.openblas.* + +# We slurp in the java code, and the native libraries needed by openblas and mkl +-includeresource: \ + @nd4j-native-api-${project.version}.jar,\ + @nd4j-native-${project.version}.jar,\ + @nd4j-native-${project.version}-${javacpp.platform}${javacpp.platform.extension}.jar,\ + @openblas-${openblas.version}-${javacpp-presets.version}-${dependency.platform}.jar!/!META-INF/versions/*,\ + @mkl-${mkl.version}-${javacpp-presets.version}-${dependency.platform2}.jar!/!META-INF/versions/*,\ + @mkl-dnn-${mkl-dnn.javacpp.version}-${dependency.platform2}.jar!/!META-INF/versions/* + + +# Set the native code location and requirements +Bundle-NativeCode: \ + org/nd4j/nativeblas/${javacpp.platform}${javacpp.platform.extension}/libnd4jcpu.${os.lib.extension};\ + org/nd4j/nativeblas/${javacpp.platform}${javacpp.platform.extension}/libjnind4jcpu.${os.lib.extension};\ + org/bytedeco/openblas/${dependency.platform}/libjniopenblas_nolapack.${os.lib.extension};\ + org/bytedeco/openblas/${dependency.platform}/libjniopenblas.${os.lib.extension};\ + org/bytedeco/openblas/${dependency.platform}/libopenblas.${os.lib.extension};\ + org/bytedeco/mkl/${dependency.platform2}/libjnimkl_rt.${os.lib.extension};\ + org/bytedeco/mkldnn/${dependency.platform2}/libgcc_s.1.${os.lib.extension};\ + org/bytedeco/mkldnn/${dependency.platform2}/libgomp.1.${os.lib.extension};\ + org/bytedeco/mkldnn/${dependency.platform2}/libiomp5.${os.lib.extension};\ + org/bytedeco/mkldnn/${dependency.platform2}/libjnimkldnn.${os.lib.extension};\ + org/bytedeco/mkldnn/${dependency.platform2}/libjnimklml.${os.lib.extension};\ + org/bytedeco/mkldnn/${dependency.platform2}/libmkldnn.0.${os.lib.extension};\ + org/bytedeco/mkldnn/${dependency.platform2}/libmklml.${os.lib.extension};\ + org/bytedeco/mkldnn/${dependency.platform2}/libstdc++.6.${os.lib.extension};\ + osname=${os.name};\ + processor=${os.arch} + + +# Advertise that we provide a backend for ND4J +Provide-Capability: org.nd4j.backend; backend.type="cpu"; version:Version="${safe.version}" \ No newline at end of file diff --git a/nd4j/nd4j-backends/nd4j-backend-impls/nd4j-native-osgi/pom.xml b/nd4j/nd4j-backends/nd4j-backend-impls/nd4j-native-osgi/pom.xml new file mode 100644 index 000000000000..0a2b13b711d7 --- /dev/null +++ b/nd4j/nd4j-backends/nd4j-backend-impls/nd4j-native-osgi/pom.xml @@ -0,0 +1,95 @@ + + + + + nd4j-backend-impls + org.nd4j + 1.0.0-SNAPSHOT + + 4.0.0 + + nd4j-native-osgi + nd4j-native-osgi + An OSGi packaging of the ND4J native backend implementation + + + + + org.nd4j + nd4j-osgi + ${project.version} + + + org.nd4j + nd4j-native + ${project.version} + + + org.nd4j + nd4j-native + ${project.version} + ${javacpp.platform} + + + + + + + + biz.aQute.bnd + bnd-maven-plugin + 4.2.0 + + + + bnd-process + + + + + + + maven-jar-plugin + + + default-jar + package + + jar + + + + lib/** + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + ${javacpp.platform}${javacpp.platform.extension} + + + + ${javacpp.platform}${javacpp.platform.extension} + + true + + + + + + + + \ No newline at end of file diff --git a/nd4j/nd4j-backends/nd4j-backend-impls/nd4j-native-platform/pom.xml b/nd4j/nd4j-backends/nd4j-backend-impls/nd4j-native-platform/pom.xml index ff85e63e7dbb..df1dd26c46e7 100644 --- a/nd4j/nd4j-backends/nd4j-backend-impls/nd4j-native-platform/pom.xml +++ b/nd4j/nd4j-backends/nd4j-backend-impls/nd4j-native-platform/pom.xml @@ -27,6 +27,7 @@ nd4j-native + nd4j-native-osgi @@ -116,6 +117,72 @@ ${project.version} ${javacpp.platform.linux-armhf} + + ${project.groupId} + ${nd4j.backend.osgi} + ${project.version} + ${javacpp.platform.android-arm} + + + ${project.groupId} + ${nd4j.backend.osgi} + ${project.version} + ${javacpp.platform.android-arm64} + + + ${project.groupId} + ${nd4j.backend.osgi} + ${project.version} + ${javacpp.platform.android-x86} + + + ${project.groupId} + ${nd4j.backend.osgi} + ${project.version} + ${javacpp.platform.android-x86_64} + + + ${project.groupId} + ${nd4j.backend.osgi} + ${project.version} + ${javacpp.platform.ios-arm64} + + + ${project.groupId} + ${nd4j.backend.osgi} + ${project.version} + ${javacpp.platform.ios-x86_64} + + + ${project.groupId} + ${nd4j.backend.osgi} + ${project.version} + ${javacpp.platform.linux-x86_64} + + + ${project.groupId} + ${nd4j.backend.osgi} + ${project.version} + ${javacpp.platform.macosx-x86_64} + + + ${project.groupId} + ${nd4j.backend.osgi} + ${project.version} + ${javacpp.platform.windows-x86_64} + + + ${project.groupId} + ${nd4j.backend.osgi} + ${project.version} + ${javacpp.platform.linux-ppc64le} + + + ${project.groupId} + ${nd4j.backend.osgi} + ${project.version} + ${javacpp.platform.linux-armhf} + diff --git a/nd4j/nd4j-backends/nd4j-backend-impls/pom.xml b/nd4j/nd4j-backends/nd4j-backend-impls/pom.xml index 86ce07ff7a6b..1834ff84c0c8 100644 --- a/nd4j/nd4j-backends/nd4j-backend-impls/pom.xml +++ b/nd4j/nd4j-backends/nd4j-backend-impls/pom.xml @@ -184,6 +184,7 @@ nd4j-native + nd4j-native-osgi nd4j-native-platform diff --git a/nd4j/nd4j-backends/nd4j-tests-osgi/bnd.bnd b/nd4j/nd4j-backends/nd4j-tests-osgi/bnd.bnd new file mode 100644 index 000000000000..66a2e2443c9d --- /dev/null +++ b/nd4j/nd4j-backends/nd4j-tests-osgi/bnd.bnd @@ -0,0 +1 @@ +Test-Cases: ${classes;NAMED;*Test} \ No newline at end of file diff --git a/nd4j/nd4j-backends/nd4j-tests-osgi/cpu.bndrun b/nd4j/nd4j-backends/nd4j-tests-osgi/cpu.bndrun new file mode 100644 index 000000000000..947ec09e1202 --- /dev/null +++ b/nd4j/nd4j-backends/nd4j-tests-osgi/cpu.bndrun @@ -0,0 +1,29 @@ +-standalone: target/index.xml +-runfw: org.apache.felix.framework;version='[6.0.3,6.0.3]' +-runsystemcapabilities: ${native_capability} +-runee: JavaSE-1.8 +-runrequires: bnd.identity;id='nd4j-tests-osgi' +-runbundles: \ + ch.qos.logback.classic;version='[1.2.3,1.2.4)',\ + ch.qos.logback.core;version='[1.2.3,1.2.4)',\ + com.google.flatbuffers.java;version='[1.10.0,1.10.1)',\ + com.google.guava;version='[20.0.0,20.0.1)',\ + com.sun.jna;version='[4.3.0,4.3.1)',\ + com.sun.jna.platform;version='[4.3.0,4.3.1)',\ + jackson;version='[1.0.0,1.0.1)',\ + joda-time;version='[2.2.0,2.2.1)',\ + nd4j-tests-osgi;version='[1.0.0,1.0.1)',\ + org.apache.commons.codec;version='[1.10.0,1.10.1)',\ + org.apache.commons.compress;version='[1.16.1,1.16.2)',\ + org.apache.commons.io;version='[2.5.0,2.5.1)',\ + org.apache.commons.lang3;version='[3.6.0,3.6.1)',\ + org.apache.commons.math3;version='[3.5.0,3.5.1)',\ + org.apache.commons.net;version='[3.1.0,3.1.1)',\ + org.apache.servicemix.bundles.junit;version='[4.12.0,4.12.1)',\ + org.nd4j.backend.native;version='[1.0.0,1.0.1)',\ + org.nd4j.frontend;version='[1.0.0,1.0.1)',\ + org.objenesis;version='[2.6.0,2.6.1)',\ + org.threeten.bp;version='[1.3.3,1.3.4)',\ + slf4j.api;version='[1.7.21,1.7.22)',\ + stax2-api;version='[3.1.4,3.1.5)',\ + uk.com.robust-it.cloning;version='[1.9.3,1.9.4)' \ No newline at end of file diff --git a/nd4j/nd4j-backends/nd4j-tests-osgi/pom.xml b/nd4j/nd4j-backends/nd4j-tests-osgi/pom.xml new file mode 100644 index 000000000000..15e5b273e121 --- /dev/null +++ b/nd4j/nd4j-backends/nd4j-tests-osgi/pom.xml @@ -0,0 +1,140 @@ + + + + + nd4j-backends + org.nd4j + 1.0.0-SNAPSHOT + + 4.0.0 + + nd4j-tests-osgi + jar + + nd4j-tests-osgi + + + 4.2.0 + + + + + org.nd4j + nd4j-osgi + ${project.version} + + + org.nd4j + nd4j-native-osgi + ${project.version} + ${javacpp.platform}${javacpp.platform.extension} + + + + org.apache.servicemix.bundles + org.apache.servicemix.bundles.junit + 4.12_1 + + + + ch.qos.logback + logback-classic + ${logback.version} + runtime + + + + ch.qos.logback + logback-core + ${logback.version} + runtime + + + + org.apache.felix + org.apache.felix.framework + 6.0.3 + runtime + + + + + + + + biz.aQute.bnd + bnd-maven-plugin + ${bnd.version} + + + + bnd-process + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.0.2 + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + true + + + + biz.aQute.bnd + bnd-indexer-maven-plugin + ${bnd.version} + + false + REQUIRED + true + + + + + index + + + + + + biz.aQute.bnd + bnd-testing-maven-plugin + ${bnd.version} + + false + + cpu.bndrun + + + + + + testing + + + + + + + \ No newline at end of file diff --git a/nd4j/nd4j-backends/nd4j-tests-osgi/src/main/java/org/nd4j/test/osgi/NDArrayCreationTest.java b/nd4j/nd4j-backends/nd4j-tests-osgi/src/main/java/org/nd4j/test/osgi/NDArrayCreationTest.java new file mode 100644 index 000000000000..8db0fdac2065 --- /dev/null +++ b/nd4j/nd4j-backends/nd4j-tests-osgi/src/main/java/org/nd4j/test/osgi/NDArrayCreationTest.java @@ -0,0 +1,88 @@ +package org.nd4j.test.osgi; +/******************************************************************************* + * Copyright (c) 2015-2018 Skymind, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + + +import static org.junit.Assert.assertEquals; + +import org.bytedeco.javacpp.FloatPointer; +import org.bytedeco.javacpp.Pointer; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.nd4j.linalg.api.buffer.DataBuffer; +import org.nd4j.linalg.api.buffer.DataType; +import org.nd4j.linalg.api.memory.MemoryWorkspace; +import org.nd4j.linalg.api.ndarray.INDArray; +import org.nd4j.linalg.factory.Nd4j; +import org.nd4j.linalg.factory.Nd4jBackend; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This test is based on the TestNDArrayCreation test from the nd4j-tests module + */ +public class NDArrayCreationTest { + + private static Logger log = LoggerFactory.getLogger(NDArrayCreationTest.class); + + @Before + public void before() throws Exception { + Nd4j nd4j = new Nd4j(); + nd4j.initWithBackend(Nd4jBackend.load()); + Nd4j.factory().setOrder('c'); + Nd4j.getExecutioner().enableDebugMode(false); + Nd4j.getExecutioner().enableVerboseMode(false); + Nd4j.setDefaultDataTypes(DataType.DOUBLE, DataType.DOUBLE); + } + + @After + public void after() throws Exception { + Nd4j.getMemoryManager().purgeCaches(); + + //Attempt to keep workspaces isolated between tests + Nd4j.getWorkspaceManager().destroyAllWorkspacesForCurrentThread(); + MemoryWorkspace currWS = Nd4j.getMemoryManager().getCurrentWorkspace(); + Nd4j.getMemoryManager().setCurrentWorkspace(null); + if(currWS != null){ + //Not really safe to continue testing under this situation... other tests will likely fail with obscure + // errors that are hard to track back to this + log.error("Open workspace leaked from test! Exiting - {}, isOpen = {} - {}", currWS.getId(), currWS.isScopeActive(), currWS); + System.exit(1); + } + } + + @Test + // We only run with CPU for now + //@Ignore("AB 2019/05/23 - Failing on linux-x86_64-cuda-9.2 - see issue #7657") + public void testBufferCreation() { + DataBuffer dataBuffer = Nd4j.createBuffer(new float[] {1, 2}); + Pointer pointer = dataBuffer.pointer(); + FloatPointer floatPointer = new FloatPointer(pointer); + DataBuffer dataBuffer1 = Nd4j.createBuffer(floatPointer, 2, DataType.FLOAT); + + assertEquals(2, dataBuffer.length()); + assertEquals(1.0, dataBuffer.getDouble(0), 1e-1); + assertEquals(2.0, dataBuffer.getDouble(1), 1e-1); + + assertEquals(2, dataBuffer1.length()); + assertEquals(1.0, dataBuffer1.getDouble(0), 1e-1); + assertEquals(2.0, dataBuffer1.getDouble(1), 1e-1); + INDArray arr = Nd4j.create(dataBuffer1); + System.out.println(arr); + } + +} diff --git a/nd4j/nd4j-backends/pom.xml b/nd4j/nd4j-backends/pom.xml index 59228e1419f3..3f90cd77a2d6 100644 --- a/nd4j/nd4j-backends/pom.xml +++ b/nd4j/nd4j-backends/pom.xml @@ -33,6 +33,7 @@ nd4j-backend-impls nd4j-api-parent nd4j-tests-tensorflow + nd4j-tests-osgi diff --git a/nd4j/nd4j-context/pom.xml b/nd4j/nd4j-context/pom.xml index 225b3784c504..896ce0d9ce99 100644 --- a/nd4j/nd4j-context/pom.xml +++ b/nd4j/nd4j-context/pom.xml @@ -33,6 +33,11 @@ nd4j-common ${project.version} + + + org.osgi + osgi.core + diff --git a/nd4j/nd4j-context/src/main/java/org/nd4j/linalg/factory/Nd4jBackend.java b/nd4j/nd4j-context/src/main/java/org/nd4j/linalg/factory/Nd4jBackend.java index bcdfba202b55..774735b97efc 100644 --- a/nd4j/nd4j-context/src/main/java/org/nd4j/linalg/factory/Nd4jBackend.java +++ b/nd4j/nd4j-context/src/main/java/org/nd4j/linalg/factory/Nd4jBackend.java @@ -21,9 +21,15 @@ import org.nd4j.config.ND4JSystemProperties; import org.nd4j.context.Nd4jContext; import org.nd4j.linalg.io.Resource; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.collect.Iterators; + import java.io.File; import java.io.IOException; import java.security.PrivilegedActionException; @@ -153,10 +159,8 @@ public abstract class Nd4jBackend { public static Nd4jBackend load() throws NoAvailableBackendException { List backends = new ArrayList<>(1); - ServiceLoader loader = ServiceLoader.load(Nd4jBackend.class); + Iterator backendIterator = getServices(); try { - - Iterator backendIterator = loader.iterator(); while (backendIterator.hasNext()) backends.add(backendIterator.next()); @@ -222,6 +226,35 @@ public int compare(Nd4jBackend o1, Nd4jBackend o2) { return load(); } + private static Iterator getServices() { + + Iterator discoveredBackends = ServiceLoader.load(Nd4jBackend.class).iterator(); + + try { + Bundle bundle = FrameworkUtil.getBundle(Nd4jBackend.class); + + + if(bundle != null) { + BundleWiring wiring = bundle.adapt(BundleWiring.class); + List requiredWires = wiring.getRequiredWires("org.nd4j.backend"); + + if(requiredWires != null) { + for(BundleWire wire : requiredWires) { + ClassLoader providerClassLoader = wire.getProviderWiring().getClassLoader(); + discoveredBackends = Iterators.concat(discoveredBackends, + ServiceLoader.load(Nd4jBackend.class, providerClassLoader).iterator()); + } + } + + } + + } catch (NoClassDefFoundError e) { + // We're not running in OSGi + } + + return discoveredBackends; + } + /** * Adds the supplied Java Archive library to java.class.path. This is benign diff --git a/nd4j/nd4j-osgi/bnd.bnd b/nd4j/nd4j-osgi/bnd.bnd index ad94bf28fea7..bf64ef6dabc3 100644 --- a/nd4j/nd4j-osgi/bnd.bnd +++ b/nd4j/nd4j-osgi/bnd.bnd @@ -3,12 +3,17 @@ Bundle-SymbolicName: org.nd4j.frontend +# A safe version to use in dependency matching +safe.version: ${maven_version;${project.version}} + +Bundle-Version: ${safe.version} + # Export all the packages from ND4J, except the ones with "impl" in the package name api.package.names: ${filterout;${packages;NAMED;org.nd4j.*|onnx|org.tensorflow.*|tensorflow*};impl} # For now, all the API packages use the same version, taken from the maven version. # Ideally the package versions would be maintained individually using annotations -api.version: ${maven_version;${project.version}} +api.version: ${safe.version} # Assemble the full export string containing all the packages, versions, and directives # Note that the suffix prevents the API packages from being substitutably imported @@ -36,13 +41,15 @@ api.export.string: ${replace;${api.package.names};$;\\;version=${api.version}\\; net.ericaro.neoitertools;version=1.0.0 # Embed the frontend jar files produced by ND4J. These must be delivered together -# due to split packages between the various jar files +# due to split packages between the various jar files. We also embed JavaCPP here +# because it contains critical non-class file resources in the properties package -includeresource: \ @nd4j-common-${project.version}.jar,\ @nd4j-context-${project.version}.jar,\ @nd4j-buffer-${project.version}.jar,\ @nd4j-api-${project.version}.jar,\ - @nd4j-native-api-${project.version}.jar + @nd4j-native-api-${project.version}.jar,\ + @javacpp-${javacpp.version}.jar!/!META-INF/* # These are non-OSGi packaged dependencies which we therefore merge # into this OSGi bundle as private copies @@ -52,11 +59,16 @@ api.export.string: ${replace;${api.package.names};$;\\;version=${api.version}\\; org.bytedeco.javacpp.*,\ net.ericaro.neoitertools.*,\ oshi.* + # These dependencies must be customised to remove them or make them # optional. The code paths that use them are not exercised by ND4J Import-Package: \ !android.content,\ !org.apache.maven.*,\ + com.sun.management;resolution:=optional,\ sun.misc;resolution:=optional,\ - * \ No newline at end of file + * + +# The ND4J API always requires a backend implementation +Require-Capability: org.nd4j.backend; filter:="(version=${api.version})"; cardinality:=multiple \ No newline at end of file diff --git a/nd4j/pom.xml b/nd4j/pom.xml index bdb0f4c9113b..3d455db7892b 100644 --- a/nd4j/pom.xml +++ b/nd4j/pom.xml @@ -88,6 +88,12 @@ ${junit.version} test + + org.osgi + osgi.core + 6.0.0 + provided + diff --git a/pom.xml b/pom.xml index 8b474e1d1a9a..6ba0759c0b47 100644 --- a/pom.xml +++ b/pom.xml @@ -868,6 +868,7 @@ linux + so @@ -879,6 +880,7 @@ macosx + dylib @@ -890,6 +892,7 @@ windows + dll